1// ========== CONFIGURACAO DOS CUSTOM SELECTS (SINGLE) ========== 2const CUSTOM_SELECT_OPTIONS = { 3 uf: { 4 placeholder: "Selecione o estado", 5 searchable: true, 6 options: [ 7 { value: "AC", label: "AC - Acre" }, 8 { value: "AL", label: "AL - Alagoas" }, 9 { value: "AP", label: "AP - Amapá" }, 10 { value: "AM", label: "AM - Amazonas" }, 11 { value: "BA", label: "BA - Bahia" }, 12 { value: "CE", label: "CE - Ceará" }, 13 { value: "DF", label: "DF - Distrito Federal" }, 14 { value: "ES", label: "ES - EspÃrito Santo" }, 15 { value: "GO", label: "GO - Goiás" }, 16 { value: "MA", label: "MA - Maranhão" }, 17 { value: "MT", label: "MT - Mato Grosso" }, 18 { value: "MS", label: "MS - Mato Grosso do Sul" }, 19 { value: "MG", label: "MG - Minas Gerais" }, 20 { value: "PA", label: "PA - Pará" }, 21 { value: "PB", label: "PB - ParaÃba" }, 22 { value: "PR", label: "PR - Paraná" }, 23 { value: "PE", label: "PE - Pernambuco" }, 24 { value: "PI", label: "PI - PiauÃ" }, 25 { value: "RJ", label: "RJ - Rio de Janeiro" }, 26 { value: "RN", label: "RN - Rio Grande do Norte" }, 27 { value: "RS", label: "RS - Rio Grande do Sul" }, 28 { value: "RO", label: "RO - Rondônia" }, 29 { value: "RR", label: "RR - Roraima" }, 30 { value: "SC", label: "SC - Santa Catarina" }, 31 { value: "SP", label: "SP - São Paulo" }, 32 { value: "SE", label: "SE - Sergipe" }, 33 { value: "TO", label: "TO - Tocantins" }, 34 ], 35 }, 36 cargo: { 37 placeholder: "Selecione o cargo", 38 searchable: false, 39 options: [ 40 { value: "proprietario", label: "Proprietário" }, 41 { value: "gerente", label: "Gerente" }, 42 { value: "orcamentista", label: "Orçamentista" }, 43 { value: "funileiro", label: "Funileiro" }, 44 { value: "preparador", label: "Preparador" }, 45 { value: "pintor", label: "Pintor" }, 46 { value: "colorista", label: "Colorista" }, 47 { value: "montador", label: "Montador" }, 48 { value: "mecanico", label: "Mecânico" }, 49 { value: "polidor", label: "Polidor" }, 50 { value: "estoquista", label: "Estoquista" }, 51 { value: "recepcionista", label: "Recepcionista" }, 52 { value: "administrativo", label: "Administrativo" }, 53 { value: "financeiro", label: "Financeiro" }, 54 { value: "outro", label: "Outro" }, 55 ], 56 }, 57 parentesco: { 58 placeholder: "Selecione", 59 searchable: false, 60 options: [ 61 { value: "conjuge", label: "Cônjuge" }, 62 { value: "filho", label: "Filho(a)" }, 63 { value: "pai", label: "Pai" }, 64 { value: "mae", label: "Mãe" }, 65 { value: "outro", label: "Outro" }, 66 ], 67 }, 68}; 69 70// ========== CONFIGURACAO DOS MULTISELECTS ========== 71const MULTISELECT_OPTIONS = { 72 "ms-sistema-orcamento": { 73 placeholder: "Selecione os sistemas", 74 allowCustom: true, 75 options: [ 76 { value: "audatex", label: "Audatex" }, 77 { value: "cilia", label: "Cilia", selected: true }, 78 { value: "orion", label: "Orion" }, 79 { value: "soma", label: "Soma" }, 80 ], 81 }, 82 "ms-sistema-gestao": { 83 placeholder: "Selecione os sistemas", 84 allowCustom: true, 85 options: [ 86 // Sistemas especializados para oficinas 87 { value: "databox", label: "DataBox" }, 88 { value: "ultracar", label: "UltraCar" }, 89 { value: "oficina-inteligente", label: "Oficina Inteligente" }, 90 { value: "oficina-integrada", label: "Oficina Integrada" }, 91 { value: "sismecanica", label: "SisMecanica (LimerSoft)" }, 92 { value: "onmotor", label: "OnMotor" }, 93 { value: "motorsw", label: "Motor SW" }, 94 { value: "hammer-system", label: "Hammer System" }, 95 { value: "gestaoclick", label: "GestaoClick" }, 96 { value: "mecanie", label: "Mecanie" }, 97 { value: "bruning", label: "Bruning Sistemas" }, 98 { value: "sige-cloud", label: "Sige Cloud" }, 99 { value: "vhsys", label: "vhsys" }, 100 { value: "empresarius", label: "Empresarius ERP" }, 101 // ERPs gerais 102 { value: "totvs", label: "TOTVS" }, 103 { value: "bling", label: "Bling" }, 104 { value: "tiny", label: "Tiny ERP" }, 105 { value: "omie", label: "Omie" }, 106 { value: "conta-azul", label: "ContaAzul" }, 107 ], 108 }, 109 "ms-seguradoras": { 110 placeholder: "Selecione ou adicione seguradoras", 111 allowCustom: true, 112 options: [ 113 { value: "nenhuma", label: "Nenhuma" }, 114 // Principais seguradoras auto Brasil 115 { value: "porto-seguro", label: "Porto Seguro" }, 116 { value: "azul", label: "Azul Seguros" }, 117 { value: "bradesco", label: "Bradesco Seguros" }, 118 { value: "sulamerica", label: "SulAmérica" }, 119 { value: "itau", label: "Itaú Seguros" }, 120 { value: "allianz", label: "Allianz" }, 121 { value: "hdi", label: "HDI Seguros" }, 122 { value: "tokio-marine", label: "Tokio Marine" }, 123 { value: "mapfre", label: "Mapfre" }, 124 { value: "liberty", label: "Liberty Seguros" }, 125 { value: "sompo", label: "Sompo Seguros" }, 126 { value: "suhai", label: "Suhai Seguradora" }, 127 { value: "youse", label: "Youse" }, 128 { value: "zurich", label: "Zurich" }, 129 { value: "chubb", label: "Chubb" }, 130 { value: "mitsui", label: "Mitsui Sumitomo" }, 131 // Locadoras/Frotas 132 { value: "localiza", label: "Localiza (Frotas)" }, 133 { value: "movida", label: "Movida (Frotas)" }, 134 { value: "unidas", label: "Unidas (Frotas)" }, 135 ], 136 }, 137 "ms-plataformas-compra": { 138 placeholder: "Selecione ou adicione plataformas", 139 allowCustom: true, 140 options: [ 141 // Plataformas especializadas autopecas 142 { value: "partsfy", label: "Partsfy" }, 143 { value: "e-peca", label: "E-Peca" }, 144 { value: "pts-parts", label: "PTS Parts" }, 145 // Fabricantes/Distribuidores 146 { value: "nakata", label: "Nakata" }, 147 { value: "cofap", label: "Cofap" }, 148 { value: "sabo", label: "Sabo" }, 149 { value: "bosch", label: "Bosch" }, 150 { value: "mahle", label: "Mahle" }, 151 { value: "ngk", label: "NGK" }, 152 { value: "valeo", label: "Valeo" }, 153 { value: "denso", label: "Denso" }, 154 { value: "continental", label: "Continental" }, 155 { value: "gates", label: "Gates" }, 156 { value: "monroe", label: "Monroe" }, 157 { value: "skf", label: "SKF" }, 158 // Redes de distribuicao 159 { value: "dpaschoal", label: "DPaschoal" }, 160 // Marketplaces gerais 161 { value: "mercado-livre", label: "Mercado Livre" }, 162 { value: "amazon", label: "Amazon" }, 163 ], 164 }, 165 "ms-notas-fiscais": { 166 placeholder: "Selecione os tipos", 167 allowCustom: false, 168 options: [ 169 { value: "nf-servicos", label: "NF de Serviços" }, 170 { value: "nf-pecas-servicos", label: "NF de Peças e Serviços" }, 171 ], 172 }, 173 "ms-tipos-veiculos": { 174 placeholder: "Selecione os tipos", 175 allowCustom: false, 176 options: [ 177 { value: "passeio", label: "VeÃculos de Passeio" }, 178 { value: "motos", label: "Motos" }, 179 { value: "utilitarios", label: "VeÃculos Utilitários" }, 180 { value: "caminhoes", label: "Caminhões" }, 181 { value: "onibus", label: "Ãnibus" }, 182 ], 183 }, 184 "ms-marcas-veiculos": { 185 placeholder: "Selecione ou adicione marcas", 186 allowCustom: true, 187 options: [ 188 // Marcas populares Brasil 189 { value: "volkswagen", label: "Volkswagen" }, 190 { value: "fiat", label: "Fiat" }, 191 { value: "chevrolet", label: "Chevrolet" }, 192 { value: "ford", label: "Ford" }, 193 { value: "toyota", label: "Toyota" }, 194 { value: "hyundai", label: "Hyundai" }, 195 { value: "honda", label: "Honda" }, 196 { value: "renault", label: "Renault" }, 197 { value: "nissan", label: "Nissan" }, 198 { value: "jeep", label: "Jeep" }, 199 { value: "peugeot", label: "Peugeot" },
200 { value: "citroen", label: "Citroën" }, 201 { value: "kia", label: "Kia" }, 202 { value: "mitsubishi", label: "Mitsubishi" }, 203 { value: "suzuki", label: "Suzuki" }, 204 { value: "subaru", label: "Subaru" }, 205 // Marcas premium/luxo 206 { value: "bmw", label: "BMW" }, 207 { value: "mercedes", label: "Mercedes-Benz" }, 208 { value: "audi", label: "Audi" }, 209 { value: "volvo", label: "Volvo" }, 210 { value: "land-rover", label: "Land Rover" }, 211 { value: "porsche", label: "Porsche" }, 212 { value: "jaguar", label: "Jaguar" }, 213 { value: "lexus", label: "Lexus" }, 214 { value: "alfa-romeo", label: "Alfa Romeo" }, 215 { value: "maserati", label: "Maserati" }, 216 { value: "ferrari", label: "Ferrari" }, 217 { value: "lamborghini", label: "Lamborghini" }, 218 { value: "bentley", label: "Bentley" }, 219 { value: "rolls-royce", label: "Rolls-Royce" }, 220 { value: "aston-martin", label: "Aston Martin" }, 221 { value: "mclaren", label: "McLaren" }, 222 { value: "mini", label: "Mini" }, 223 // Marcas americanas 224 { value: "ram", label: "RAM" }, 225 { value: "dodge", label: "Dodge" }, 226 { value: "chrysler", label: "Chrysler" }, 227 { value: "tesla", label: "Tesla" }, 228 // Marcas chinesas 229 { value: "caoa-chery", label: "Caoa Chery" }, 230 { value: "byd", label: "BYD" }, 231 { value: "gwm", label: "GWM" }, 232 { value: "jac", label: "JAC Motors" }, 233 { value: "lifan", label: "Lifan" }, 234 { value: "haval", label: "Haval" }, 235 { value: "shineray", label: "Shineray" }, 236 // Marcas coreanas 237 { value: "ssangyong", label: "SsangYong" }, 238 // Marcas caminhoes/comerciais 239 { value: "scania", label: "Scania" }, 240 { value: "volvo-trucks", label: "Volvo Caminhões" }, 241 { value: "mercedes-trucks", label: "Mercedes-Benz Caminhões" }, 242 { value: "man", label: "MAN" }, 243 { value: "daf", label: "DAF" }, 244 { value: "iveco", label: "Iveco" }, 245 { value: "vw-caminhoes", label: "VW Caminhões" },
246 { value: "ford-trucks", label: "Ford Caminhões" }, 247 // Marcas de motos 248 { value: "honda-motos", label: "Honda Motos" }, 249 { value: "yamaha", label: "Yamaha" }, 250 { value: "suzuki-motos", label: "Suzuki Motos" }, 251 { value: "kawasaki", label: "Kawasaki" }, 252 { value: "bmw-motorrad", label: "BMW Motorrad" }, 253 { value: "harley-davidson", label: "Harley-Davidson" }, 254 { value: "ducati", label: "Ducati" }, 255 { value: "triumph", label: "Triumph" }, 256 { value: "royal-enfield", label: "Royal Enfield" }, 257 { value: "ktm", label: "KTM" }, 258 { value: "dafra", label: "Dafra" }, 259 { value: "shineray-motos", label: "Shineray Motos" }, 260 { value: "haojue", label: "Haojue" }, 261 // Genéricos 262 { value: "multimarcas", label: "Multimarcas" }, 263 { value: "outros", label: "Outros" }, 264 ], 265 }, 266 "ms-especializacoes": { 267 placeholder: "Selecione as especializações", 268 allowCustom: false, 269 options: [ 270 { value: "blindados", label: "Blindados" }, 271 { value: "funilaria-aluminio", label: "Funilaria de AlumÃnio" }, 272 { value: "fibra-vidro", label: "Fibra de Vidro" }, 273 { value: "veiculos-eletrificados", label: "VeÃculos Eletrificados" }, 274 { value: "veiculos-premium", label: "VeÃculos Premium" }, 275 { value: "martelinho-ouro", label: "Martelinho de Ouro" }, 276 { value: "instalacao-vidros", label: "Instalação de Vidros" }, 277 { value: "mecanica", label: "Mecânica" }, 278 { value: "cambio-automatico", label: "Câmbio Automático" }, 279 { value: "estetica-automotiva", label: "Estética Automotiva" }, 280 ], 281 }, 282 "ms-banheiros": { 283 placeholder: "Selecione os tipos", 284 allowCustom: false, 285 options: [ 286 { value: "feminino", label: "Feminino" }, 287 { value: "masculino", label: "Masculino" }, 288 { value: "unissex", label: "Unissex" }, 289 ], 290 }, 291 "ms-tipos-solda": { 292 placeholder: "Selecione os tipos", 293 allowCustom: false, 294 options: [ 295 { value: "mig-mag", label: "MIG/MAG" }, 296 { value: "solda-ponto", label: "Solda Ponto" }, 297 { value: "oxiacetileno", label: "Oxiacetileno" }, 298 { value: "tig", label: "TIG" }, 299 { value: "outros", label: "Outros" }, 300 ], 301 }, 302 "ms-tipo-lixamento": { 303 placeholder: "Selecione os tipos", 304 allowCustom: false, 305 options: [ 306 { value: "umido", label: "Ãmido" }, 307 { value: "seco", label: "Seco" }, 308 ], 309 }, 310}; 311 312// ========== ESTADO DA APLICACAO ========== 313let currentStep = 1; 314const totalSteps = 6; 315let colaboradorCount = 1; 316 317// Labels dos steps para exibição mobile 318const STEP_LABELS = { 319 1: "Dados da Oficina", 320 2: "Operação", 321 3: "Estrutura", 322 4: "Ferramental", 323 5: "Fotos", 324 6: "Saúde", 325}; 326 327// ========== NAVEGACAO ENTRE STEPS ========== 328function goToStep(step) { 329 if (step < 1 || step > totalSteps) return; 330 331 // Valida step atual antes de avançar (não valida ao voltar) 332 if (step > currentStep) { 333 const errors = validateStep(currentStep); 334 if (errors.length > 0) { 335 showAlertModal({ 336 type: "error", 337 title: "Campos obrigatórios", 338 message: "Preencha todos os campos obrigatórios para continuar.", 339 list: errors, 340 }); 341 return; 342 } 343 } 344 345 // Remove active do step atual 346 const currentStepEl = document.getElementById(`step${currentStep}`); 347 if (currentStepEl) { 348 currentStepEl.classList.remove("active-step"); 349 } 350 351 // Esconde conteudo atual 352 document 353 .getElementById(`step${currentStep}-content`) 354 .classList.remove("active"); 355 356 // Marca steps anteriores como completed 357 for (let i = 1; i < step; i++) { 358 const prevStep = document.getElementById(`step${i}`); 359 if (prevStep && !prevStep.classList.contains("completed-step")) { 360 prevStep.classList.add("completed-step"); 361 const stepNumber = prevStep.querySelector(".step-number"); 362 if (stepNumber) { 363 stepNumber.innerHTML = '<i class="pi pi-check"></i>'; 364 } 365 } 366 } 367 368 // Remove completed dos steps futuros (caso usuario volte) 369 for (let i = step; i <= totalSteps; i++) { 370 const futureStep = document.getElementById(`step${i}`); 371 if (futureStep) { 372 futureStep.classList.remove("completed-step"); 373 const stepNumber = futureStep.querySelector(".step-number"); 374 if (stepNumber && stepNumber.innerHTML.includes("pi-check")) { 375 stepNumber.innerHTML = i; 376 } 377 } 378 } 379 380 // Mostra novo step 381 currentStep = step; 382 const newStepEl = document.getElementById(`step${currentStep}`); 383 if (newStepEl) { 384 newStepEl.classList.add("active-step"); 385 newStepEl.classList.remove("completed-step"); 386 const stepNumber = newStepEl.querySelector(".step-number"); 387 if (stepNumber) { 388 stepNumber.innerHTML = currentStep; 389 } 390 } 391 392 document.getElementById(`step${currentStep}-content`).classList.add("active"); 393 394 // Atualiza progress bar animada 395 updateProgressBar(); 396 397 // Atualiza info mobile 398 updateMobileProgressInfo(); 399 400 // Salva lead parcial ao avançar de etapa 401 sendPartialLead(); 402 403 // Scroll to top 404 window.scrollTo({ top: 0, behavior: "smooth" }); 405} 406 407// Atualiza a barra de progresso animada 408function updateProgressBar() { 409 const progressBarFill = document.getElementById("progress-bar-fill"); 410 if (progressBarFill) { 411 progressBarFill.setAttribute("data-step", currentStep); 412 } 413} 414 415function updateMobileProgressInfo() { 416 const mobileInfo = document.querySelector(".mobile-progress-info"); 417 if (mobileInfo) { 418 mobileInfo.innerHTML = `
419 <span class="current-step">Etapa ${currentStep} de ${totalSteps}</span> 420 <span> â ${STEP_LABELS[currentStep]}</span> 421 `; 422 } 423} 424 425// ========== TOGGLE BUTTONS SIM/NAO ========== 426function toggleButton(btn, activeClass) { 427 const group = btn.parentElement; 428 group.querySelectorAll(".toggle-btn").forEach((b) => { 429 b.classList.remove("active", "active-green", "active-red"); 430 }); 431 btn.classList.add(activeClass); 432} 433 434// ========== SECOES COLAPSAVEIS ========== 435function toggleCollapsible(header) { 436 header.classList.toggle("open"); 437 header.nextElementSibling.classList.toggle("open"); 438} 439 440// ========== UPLOAD DE FOTOS ========== 441function markUploaded(input) { 442 if (input.files.length > 0) { 443 const item = input.closest(".photo-upload-item"); 444 item.classList.add("has-file"); 445 item.querySelector("i").className = "pi pi-check-circle"; 446 } 447} 448 449// ========== COLABORADORES ========== 450function toggleColaborador(header) { 451 const card = header.closest(".colaborador-card"); 452 card.classList.toggle("collapsed"); 453} 454 455function addColaborador() { 456 // Colapsa todos os cards existentes 457 document.querySelectorAll("#colaboradores-container .colaborador-card").forEach((card) => { 458 card.classList.add("collapsed"); 459 }); 460 colaboradorCount++; 461 const container = document.getElementById("colaboradores-container"); 462 const firstCard = container.querySelector(".colaborador-card"); 463 const newCard = firstCard.cloneNode(true); 464 465 // Atualiza titulo 466 newCard.querySelector("h4").innerHTML = 467 `<i class="pi pi-user"></i> Colaborador ${colaboradorCount}`; 468 469 // Mostra botao de remover 470 newCard.querySelector(".btn-remove").style.display = "block"; 471 472 // Limpa valores de inputs 473 newCard 474 .querySelectorAll('input:not([type="hidden"])') 475 .forEach((input) => (input.value = "")); 476 477 // Reseta custom-selects 478 newCard.querySelectorAll(".custom-select").forEach((cs) => { 479 const configKey = cs.dataset.config; 480 const config = CUSTOM_SELECT_OPTIONS[configKey]; 481 if (config) { 482 const trigger = cs.querySelector(".custom-select-trigger"); 483 trigger.innerHTML = `<span class="custom-select-placeholder">${config.placeholder}</span>`; 484 cs.querySelectorAll(".custom-select-option").forEach((opt) => 485 opt.classList.remove("selected"), 486 ); 487 cs.querySelector('input[type="hidden"]').value = ""; 488 } 489 }); 490 491 // Limpa dependentes exceto o primeiro (se existir) 492 const depList = newCard.querySelector(".dependentes-list"); 493 if (depList) { 494 const depRows = depList.querySelectorAll(".dependente-row"); 495 depRows.forEach((row, i) => { 496 if (i > 0) row.remove(); 497 else { 498 row 499 .querySelectorAll('input:not([type="hidden"])') 500 .forEach((input) => (input.value = "")); 501 // Reseta custom-selects do dependente 502 row.querySelectorAll(".custom-select").forEach((cs) => { 503 const configKey = cs.dataset.config; 504 const config = CUSTOM_SELECT_OPTIONS[configKey]; 505 if (config) { 506 const trigger = cs.querySelector(".custom-select-trigger"); 507 trigger.innerHTML = `<span class="custom-select-placeholder">${config.placeholder}</span>`; 508 cs.querySelectorAll(".custom-select-option").forEach((opt) => 509 opt.classList.remove("selected"), 510 ); 511 cs.querySelector('input[type="hidden"]').value = ""; 512 } 513 }); 514 } 515 }); 516 } 517 518 // Garante que o novo card está expandido 519 newCard.classList.remove("collapsed"); 520 521 container.appendChild(newCard); 522} 523 524function removeColaborador(btn) { 525 btn.closest(".colaborador-card").remove(); 526} 527 528// ========== DEPENDENTES ========== 529function addDependente(btn) { 530 const list = btn.previousElementSibling; 531 const firstRow = list.querySelector(".dependente-row"); 532 const newRow = firstRow.cloneNode(true); 533 534 // Limpa inputs 535 newRow 536 .querySelectorAll('input:not([type="hidden"])') 537 .forEach((input) => (input.value = "")); 538 539 // Reseta custom-selects 540 newRow.querySelectorAll(".custom-select").forEach((cs) => { 541 const configKey = cs.dataset.config; 542 const config = CUSTOM_SELECT_OPTIONS[configKey]; 543 if (config) { 544 const trigger = cs.querySelector(".custom-select-trigger"); 545 trigger.innerHTML = `<span class="custom-select-placeholder">${config.placeholder}</span>`;
546 cs.querySelectorAll(".custom-select-option").forEach((opt) => 547 opt.classList.remove("selected"), 548 ); 549 cs.querySelector('input[type="hidden"]').value = ""; 550 } 551 }); 552 553 list.appendChild(newRow); 554} 555 556// ========== CONFIGURACAO DA API ========== 557const API_URL = "https://script.google.com/macros/s/AKfycbw-00hBrW4AeN302lhnn93WxsMo0kme1K-gcxuRcHL5v6bh4vz_VTWL0gXnNgzI2Ii6/exec"; 558 559// Lead parcial: lê da tag script (injetado pelo Vue) ou usa Google Apps Script como fallback 560const _scriptTag = document.querySelector('script[src*="clubedeoficinas/script.js"]'); 561const LEAD_API_URL = (_scriptTag && _scriptTag.dataset.apiUrl) || ""; 562const LEAD_API_KEY = (_scriptTag && _scriptTag.dataset.apiKey) || ""; 563 564// ========== ALERTAS (MODAL) ========== 565let lastFocusedElementBeforeAlertModal = null; 566let alertModalEscListenerBound = false; 567 568function bindAlertModalInteractions(overlay) { 569 if (!overlay || overlay.dataset.bound === "true") return; 570 overlay.dataset.bound = "true"; 571 572 overlay.addEventListener("click", (e) => { 573 if (e.target === overlay) closeAlertModal(); 574 }); 575 576 if (!alertModalEscListenerBound) { 577 alertModalEscListenerBound = true; 578 document.addEventListener("keydown", (e) => { 579 const currentOverlay = document.getElementById("alert-modal"); 580 if ( 581 e.key === "Escape" && 582 currentOverlay && 583 currentOverlay.classList.contains("open") 584 ) { 585 closeAlertModal(); 586 } 587 }); 588 } 589} 590 591function ensureAlertModal() { 592 let overlay = document.getElementById("alert-modal"); 593 if (!overlay) { 594 overlay = document.createElement("div"); 595 overlay.className = "alert-modal-overlay"; 596 overlay.id = "alert-modal"; 597 overlay.setAttribute("aria-hidden", "true"); 598 overlay.innerHTML = ` 599 <div class="alert-modal alert-modal--info" role="dialog" aria-modal="true" aria-labelledby="alert-modal-title-text" aria-describedby="alert-modal-message"> 600 <div class="alert-modal-header"> 601 <h3 class="alert-modal-title"> 602 <i class="pi pi-info-circle" id="alert-modal-icon"></i> 603 <span id="alert-modal-title-text">Aviso</span> 604 </h3> 605 <button type="button" class="alert-modal-close" aria-label="Fechar" onclick="closeAlertModal()"> 606 <i class="pi pi-times"></i> 607 </button> 608 </div> 609 <div class="alert-modal-body"> 610 <p class="alert-modal-message" id="alert-modal-message"></p> 611 <ul class="alert-modal-list" id="alert-modal-list" hidden></ul> 612 </div> 613 <div class="alert-modal-actions"> 614 <button type="button" class="btn btn-primary" id="alert-modal-primary" onclick="closeAlertModal()"> 615 Ok 616 </button> 617 </div> 618 </div> 619 `; 620 document.body.appendChild(overlay); 621 } 622 623 bindAlertModalInteractions(overlay); 624 return overlay; 625} 626 627function showAlertModal({ 628 type = "info", 629 title = "Aviso", 630 message = "", 631 list, 632} = {}) { 633 const overlay = ensureAlertModal(); 634 635 const modal = overlay.querySelector(".alert-modal"); 636 const iconEl = document.getElementById("alert-modal-icon"); 637 const titleEl = document.getElementById("alert-modal-title-text"); 638 const messageEl = document.getElementById("alert-modal-message"); 639 const listEl = document.getElementById("alert-modal-list"); 640 const primaryBtn = document.getElementById("alert-modal-primary"); 641 642 const normalizedType = ["info", "success", "error"].includes(type) 643 ? type 644 : "info"; 645 646 if (modal) { 647 modal.classList.remove( 648 "alert-modal--info", 649 "alert-modal--success", 650 "alert-modal--error", 651 ); 652 modal.classList.add(`alert-modal--${normalizedType}`); 653 } 654 655 if (iconEl) { 656 const iconByType = { 657 info: "pi-info-circle", 658 success: "pi-check-circle", 659 error: "pi-exclamation-triangle", 660 }; 661 iconEl.className = `pi ${iconByType[normalizedType] || iconByType.info}`; 662 } 663 664 if (titleEl) titleEl.textContent = title || "Aviso"; 665 if (messageEl) messageEl.textContent = message || ""; 666 667 if (listEl) { 668 listEl.innerHTML = ""; 669 const items = Array.isArray(list) ? list.filter(Boolean) : [];
670 items.forEach((item) => { 671 const li = document.createElement("li"); 672 li.textContent = item; 673 listEl.appendChild(li); 674 }); 675 listEl.hidden = items.length === 0; 676 } 677 678 lastFocusedElementBeforeAlertModal = document.activeElement; 679 680 overlay.classList.add("open"); 681 overlay.setAttribute("aria-hidden", "false"); 682 document.body.classList.add("alert-modal-open"); 683 684 if (primaryBtn) primaryBtn.focus(); 685} 686 687function closeAlertModal() { 688 const overlay = document.getElementById("alert-modal"); 689 if (!overlay) return; 690 691 overlay.classList.remove("open"); 692 overlay.setAttribute("aria-hidden", "true"); 693 document.body.classList.remove("alert-modal-open"); 694 695 const toFocus = lastFocusedElementBeforeAlertModal; 696 lastFocusedElementBeforeAlertModal = null; 697 if (toFocus && typeof toFocus.focus === "function") { 698 toFocus.focus(); 699 } 700} 701 702// ========== SUBMIT FORM ========== 703async function submitForm() { 704 const submitBtn = document.querySelector(".btn-success"); 705 const originalText = submitBtn.innerHTML; 706 707 try { 708 // Desabilita botao e mostra loading 709 submitBtn.disabled = true; 710 submitBtn.innerHTML = '<i class="pi pi-spin pi-spinner"></i> Enviando...'; 711 712 // Coleta todos os dados 713 const formData = collectFormData(); 714 console.log("Dados coletados:", formData); 715 716 // Validacao basica 717 const validation = validateFormData(formData); 718 if (!validation.valid) { 719 const requiredFields = (validation.errors || []) 720 .map((e) => e.replace(/^\s*-\s*/, "").trim()) 721 .filter(Boolean); 722 showAlertModal({ 723 type: "error", 724 title: "Campos obrigatórios", 725 message: "Por favor, preencha os campos obrigatórios para continuar.", 726 list: requiredFields, 727 }); 728 submitBtn.disabled = false; 729 submitBtn.innerHTML = originalText; 730 return; 731 } 732 733 // Se nao tiver API configurada, apenas loga 734 if (!API_URL) { 735 console.log("API_URL nao configurada. Dados do formulario:", formData); 736 showAlertModal({ 737 type: "info", 738 title: "Modo desenvolvimento", 739 message: 740 "API não configurada. Os dados foram registrados no console para teste.", 741 }); 742 submitBtn.disabled = false; 743 submitBtn.innerHTML = originalText; 744 return; 745 } 746 747 // Fotos: converte para Base64 e envia para Google Drive 748 submitBtn.innerHTML = 749 '<i class="pi pi-spin pi-spinner"></i> Processando fotos...'; 750 formData.fotos = await collectPhotosBase64(); 751 752 submitBtn.innerHTML = '<i class="pi pi-spin pi-spinner"></i> Enviando...'; 753 754 // Envia para Google Apps Script (planilha principal) 755 await fetch(API_URL, { 756 method: "POST", 757 mode: "no-cors", 758 headers: { "Content-Type": "application/json" }, 759 body: JSON.stringify(formData), 760 }); 761 762 // Com no-cors não conseguimos ler a resposta, mas se não der erro, funcionou 763 formSubmitted = true; 764 clearDraft(); 765 window.location.assign("sucesso"); 766 767 // Redireciona para página de sucesso ou inÃcio 768 // window.location.href = 'index.html'; 769 } catch (error) { 770 console.error("Erro no envio:", error); 771 showAlertModal({ 772 type: "error", 773 title: "Erro ao enviar cadastro", 774 message: "Não foi possÃvel enviar. Por favor, tente novamente.", 775 }); 776 } finally { 777 submitBtn.disabled = false; 778 submitBtn.innerHTML = originalText; 779 } 780} 781 782// ========== COLETA DE DADOS ========== 783function collectFormData() { 784 const data = { 785 dadosCadastrais: collectDadosCadastrais(), 786 operacao: collectOperacao(), 787 estrutura: collectEstrutura(), 788 ferramental: collectFerramental(), 789 fotos: {}, // Preenchido no upload 790 colaboradores: collectColaboradores(), 791 }; 792 793 return data; 794} 795 796function collectDadosCadastrais() { 797 const logradouro = getInputValue("endereco"); 798 const numero = getInputValue("endereco-numero"); 799 const complemento = getInputValue("endereco-complemento"); 800 const enderecoCompleto = [logradouro, numero, complemento] 801 .filter(Boolean) 802 .join(", "); 803 804 return { 805 razaoSocial: getInputValue("razao-social"),
806 cnpj: getInputValue("cnpj"), 807 nomeFantasia: getInputValue("nome-fantasia"), 808 inscricaoEstadual: getInputValue("inscricao-estadual"), 809 inscricaoMunicipal: getInputValue("inscricao-municipal"), 810 endereco: enderecoCompleto, 811 logradouro, 812 numero, 813 complemento, 814 cep: getInputValue("cep"), 815 bairro: getInputValue("bairro"), 816 cidade: getInputValue("cidade"), 817 uf: getSelectValue("uf-oficina"), 818 proprietario: getInputValue("proprietario"), 819 telefone: getInputValue("telefone"), 820 email: getInputValue("email"), 821 orcamentista: { 822 nome: getInputValue("orcamentista-nome"), 823 telefone: getInputValue("orcamentista-telefone"), 824 email: getInputValue("orcamentista-email"), 825 }, 826 compradorPecas: { 827 nome: getInputValue("comprador-nome"), 828 telefone: getInputValue("comprador-telefone"), 829 email: getInputValue("comprador-email"), 830 }, 831 }; 832} 833 834function collectOperacao() { 835 return { 836 credenciadaSeguradoras: getToggleValue("credenciada-seguradoras"), 837 credenciadaAssociacoes: getToggleValue("credenciada-associacoes"), 838 seguradoras: getMultiselectValues("ms-seguradoras"), 839 passagemParticulares: parseInt(getInputValue("passagem-particulares")) || 0, 840 passagemCredenciadas: parseInt(getInputValue("passagem-credenciadas")) || 0, 841 possuiSistemaOrcamento: getToggleValue("possui-sistema-orcamento"), 842 sistemasOrcamento: getMultiselectValues("ms-sistema-orcamento"), 843 possuiSistemaGestao: getToggleValue("possui-sistema-gestao"), 844 sistemasGestao: getMultiselectValues("ms-sistema-gestao"), 845 utilizaPlataformaCompra: getToggleValue("utiliza-plataforma-compra"), 846 plataformasCompra: getMultiselectValues("ms-plataformas-compra"), 847 aceitaFornecimentoPecas: getToggleValue("aceita-fornecimento-pecas"), 848 aceitaCartoes: getToggleValue("aceita-cartoes"), 849 parcelaFranquia: getToggleValue("parcela-franquia"), 850 quantasParcelas: parseInt(getInputValue("quantas-parcelas")) || 0, 851 utilizaAntecipacao: getToggleValue("utiliza-antecipacao"), 852 tiposNotaFiscal: getMultiselectValues("ms-notas-fiscais"), 853 recebe24horas: getToggleValue("recebe-24horas"), 854 aceitaPlacaClube: getToggleValue("aceita-placa-clube"), 855 tiposVeiculos: getMultiselectValues("ms-tipos-veiculos"), 856 marcasVeiculos: getMultiselectValues("ms-marcas-veiculos"), 857 especializacoes: getMultiselectValues("ms-especializacoes"), 858 }; 859} 860 861function collectEstrutura() { 862 return { 863 profissionais: collectProfissionais(), 864 suporte: collectSuporte(), 865 infraestrutura: { 866 possuiRecepcao: getToggleValue("possui-recepcao"), 867 banheiros: getMultiselectValues("ms-banheiros"), 868 localPecas: getToggleValue("possui-local-pecas"), 869 localSucatas: getToggleValue("possui-local-sucatas"), 870 localResiduos: getToggleValue("possui-local-residuos"), 871 }, 872 }; 873} 874 875function collectProfissionais() { 876 const profissionais = {}; 877 const rows = document.querySelectorAll( 878 "#step3-content .professionals-table:first-of-type tbody tr", 879 ); 880 const funcoes = [ 881 "orcamentista", 882 "compradorPecas", 883 "funileiro", 884 "preparador", 885 "pintor", 886 "colorista", 887 "montador", 888 "estoquista", 889 "mecanico", 890 "polidor", 891 "martelinho", 892 ]; 893 894 rows.forEach((row, index) => { 895 if (funcoes[index]) { 896 const inputs = row.querySelectorAll('input[type="number"]'); 897 profissionais[funcoes[index]] = { 898 qtdProprio: parseInt(inputs[0]?.value) || 0, 899 qtdTerceirizado: parseInt(inputs[1]?.value) || 0, 900 }; 901 } 902 }); 903 904 return profissionais; 905} 906 907function collectSuporte() { 908 const suporte = {}; 909 const rows = document.querySelectorAll( 910 "#step3-content .professionals-table:last-of-type tbody tr", 911 ); 912 const funcoes = ["recepcao", "financeiro", "administrativo"]; 913 914 rows.forEach((row, index) => { 915 if (funcoes[index]) { 916 const inputs = row.querySelectorAll('input[type="number"]'); 917 suporte[funcoes[index]] = { 918 qtdProprio: parseInt(inputs[0]?.value) || 0, 919 qtdTerceirizado: parseInt(inputs[1]?.value) || 0, 920 }; 921 } 922 }); 923 924 return suporte; 925} 926 927function collectFerramental() { 928 const hasScanner = getToggleValue("scanner"); 929 930 return { 931 funilaria: { 932 mesaEstiramento: getToggleValue("mesa-estiramento"), 933 ciborg: getToggleValue("ciborg"), 934 spotter: getToggleValue("spotter"), 935 tiposSolda: getMultiselectValues("ms-tipos-solda"), 936 }, 937 pintura: { 938 mixerTintas: getToggleValue("mixer-tintas"), 939 baseAgua: getToggleValue("base-agua"),
940 marcasTinta: getInputValue("marcas-tinta"), 941 tipoLixamento: getMultiselectValues("ms-tipo-lixamento"), 942 planoAspirante: getToggleValue("plano-aspirante"), 943 cabinePintura: getToggleValue("cabine-pintura"), 944 cabineAquecimento: getToggleValue("cabine-aquecimento"), 945 }, 946 diagnostico: { 947 scanner: hasScanner, 948 marcaScanner: hasScanner ? getInputValue("scanner-marca") : "", 949 modeloScanner: hasScanner ? getInputValue("scanner-modelo") : "", 950 }, 951 reparacaoEspecial: { 952 ferramentasPlastico: getToggleValue("ferramentas-plastico"), 953 marcaPlastico: getInputValue("marca-plastico"), 954 ferramentasAluminio: getToggleValue("ferramentas-aluminio"), 955 marcaAluminio: getInputValue("marca-aluminio"), 956 }, 957 }; 958} 959 960function collectColaboradores() { 961 const colaboradores = []; 962 const cards = document.querySelectorAll(".colaborador-card"); 963 964 cards.forEach((card) => { 965 const fields = card.querySelectorAll(".form-grid .field"); 966 const getInput = (idx) => 967 fields[idx]?.querySelector('input:not([type="hidden"])')?.value?.trim() || 968 ""; 969 const getSelect = (idx) => 970 getCustomSelectValue(fields[idx]?.querySelector(".custom-select")) || 971 fields[idx]?.querySelector("select")?.value || 972 ""; 973 const colaborador = { 974 nome: getInput(0), 975 cargo: getSelect(1), 976 cpf: getInput(2), 977 dataNascimento: getInput(3), 978 endereco: getInput(4),
979 bairro: getInput(5), 980 cidade: getInput(6), 981 uf: getSelect(7), 982 cep: getInput(8), 983 email: getInput(9), 984 telefone: getInput(10), 985 dependentes: collectDependentes(card), 986 }; 987 colaboradores.push(colaborador); 988 }); 989 990 return colaboradores; 991} 992 993function collectDependentes(card) { 994 const dependentes = []; 995 const rows = card.querySelectorAll(".dependente-row"); 996 997 rows.forEach((row) => { 998 const inputs = row.querySelectorAll('input:not([type="hidden"])'); 999 const parentesco = 1000 getCustomSelectValue( 1001 row.querySelector('.custom-select[data-config="parentesco"]'), 1002 ) || 1003 row.querySelector("select")?.value || 1004 ""; 1005 1006 if (inputs[0]?.value) { 1007 // So adiciona se tiver nome 1008 dependentes.push({ 1009 nome: inputs[0]?.value || "", 1010 parentesco: parentesco, 1011 cpf: inputs[1]?.value || "", 1012 dataNascimento: inputs[2]?.value || "", 1013 }); 1014 } 1015 }); 1016 1017 return dependentes; 1018} 1019 1020// ========== HELPERS DE COLETA ========== 1021function getInputValue(id) { 1022 const el = document.getElementById(id); 1023 return el ? el.value.trim() : ""; 1024} 1025 1026function setInputValue(id, value) { 1027 const el = document.getElementById(id); 1028 if (!el) return; 1029 el.value = value || ""; 1030} 1031 1032function getSelectValue(id) { 1033 const el = document.getElementById(id); 1034 if (!el) return ""; 1035 // Verifica se foi convertido para custom-select 1036 const customSelect = el.closest ? el.closest(".custom-select") : null; 1037 if (customSelect) { 1038 return getCustomSelectValue(customSelect); 1039 } 1040 return el.value || ""; 1041} 1042 1043function setSelectValue(id, value) { 1044 const el = document.getElementById(id); 1045 if (!el) return; 1046 // Verifica se foi convertido para custom-select 1047 const customSelect = el.closest ? el.closest(".custom-select") : null; 1048 if (customSelect) { 1049 setCustomSelectValue(customSelect, value); 1050 return; 1051 } 1052 el.value = value || ""; 1053} 1054 1055function getCustomSelectValue(container) { 1056 if (!container) return ""; 1057 const hidden = container.querySelector('input[type="hidden"]'); 1058 return hidden ? hidden.value : ""; 1059} 1060 1061function setCustomSelectValue(container, value) { 1062 if (!container) return; 1063 1064 const normalizedValue = (value || "").toString().trim().toUpperCase(); 1065 const optionEl = container.querySelector( 1066 `.custom-select-option[data-value="${normalizedValue}"]`, 1067 ); 1068 if (!optionEl) return; 1069 1070 container.querySelectorAll(".custom-select-option").forEach((opt) => { 1071 opt.classList.remove("selected"); 1072 }); 1073 optionEl.classList.add("selected"); 1074 1075 const trigger = container.querySelector(".custom-select-trigger"); 1076 if (trigger) { 1077 trigger.innerHTML = `<span class="custom-select-value">${optionEl.textContent.trim()}</span>`; 1078 } 1079 1080 const hidden = container.querySelector('input[type="hidden"]'); 1081 if (hidden) hidden.value = normalizedValue; 1082} 1083 1084function getToggleValue(dataToggle) { 1085 const group = document.querySelector(`[data-toggle="${dataToggle}"]`); 1086 if (!group) return null; 1087 const activeBtn = group.querySelector(".toggle-btn.active-green"); 1088 return activeBtn !== null; 1089} 1090 1091function getMultiselectValues(id) { 1092 const container = document.getElementById(id); 1093 if (!container) return []; 1094 1095 const selectedOptions = container.querySelectorAll( 1096 ".multiselect-option.selected", 1097 ); 1098 return Array.from(selectedOptions).map((opt) => ({ 1099 value: opt.dataset.value, 1100 label: opt.querySelector("span")?.textContent || "", 1101 isCustom: opt.dataset.custom === "true", 1102 })); 1103} 1104 1105function setToggleValue(dataToggle, value) { 1106 const group = document.querySelector(`[data-toggle="${dataToggle}"]`); 1107 if (!group || value === null || value === undefined) return; 1108 const btns = group.querySelectorAll(".toggle-btn"); 1109 btns.forEach((b) => b.classList.remove("active", "active-green", "active-red")); 1110 if (btns.length >= 2) { 1111 if (value === true) btns[0].classList.add("active-green"); 1112 else btns[1].classList.add("active-red"); 1113 } 1114} 1115 1116function setMultiselectValues(id, values) { 1117 const container = document.getElementById(id); 1118 if (!container || !Array.isArray(values)) return; 1119 container.querySelectorAll(".multiselect-option").forEach((opt) => opt.classList.remove("selected")); 1120 const valSet = new Set(values.map((v) => (typeof v === "string" ? v : v.value)));
1121 values.forEach((v) => { 1122 const val = typeof v === "string" ? v : v.value; 1123 const label = typeof v === "string" ? v : v.label || v.value; 1124 const isCustom = typeof v === "object" && v.isCustom; 1125 if (isCustom && !container.querySelector(`.multiselect-option[data-value="${val}"]`)) { 1126 const opts = container.querySelector(".multiselect-options"); 1127 if (opts) { 1128 const el = document.createElement("div"); 1129 el.className = "multiselect-option"; 1130 el.dataset.value = val; 1131 el.dataset.custom = "true"; 1132 el.onclick = function () { selectOption(id, this); }; 1133 el.innerHTML = `<div class="multiselect-option-checkbox"></div><span>${label}</span>`; 1134 opts.appendChild(el); 1135 valSet.add(val); 1136 } 1137 } 1138 }); 1139 container.querySelectorAll(".multiselect-option").forEach((opt) => { 1140 if (valSet.has(opt.dataset.value)) opt.classList.add("selected"); 1141 }); 1142 updateMultiselectTrigger(id); 1143} 1144 1145// ========== FOTOS (V2 - conversão para Base64 e upload para Google Drive) ========== 1146 1147// Converte um arquivo para Base64 1148function fileToBase64(file) { 1149 return new Promise((resolve, reject) => { 1150 const reader = new FileReader(); 1151 reader.onload = () => resolve(reader.result); 1152 reader.onerror = (error) => reject(error); 1153 reader.readAsDataURL(file); 1154 }); 1155} 1156 1157// Coleta todas as fotos e converte para Base64 1158async function collectPhotosBase64() { 1159 const photoInputs = document.querySelectorAll( 1160 '#step5-content input[type="file"]', 1161 ); 1162 const photos = {}; 1163
1164 for (const input of photoInputs) { 1165 const field = input.dataset.field; 1166 if (field && input.files.length > 0) { 1167 const file = input.files[0]; 1168 try { 1169 const base64 = await fileToBase64(file); 1170 photos[field] = { 1171 base64: base64, 1172 filename: file.name, 1173 mimeType: file.type || "image/jpeg", 1174 }; 1175 } catch (error) { 1176 console.error(`Erro ao converter foto ${field}:`, error); 1177 } 1178 } 1179 } 1180 1181 return photos; 1182} 1183 1184// ========== VALIDACAO ========== 1185 1186function requireToggle(dataToggle, label) { 1187 const val = getToggleValue(dataToggle); 1188 return val === null ? label : null; 1189} 1190 1191function requireInput(id, label) { 1192 return getInputValue(id) ? null : label; 1193} 1194 1195function requireSelect(id, label) { 1196 return getSelectValue(id) ? null : label; 1197} 1198 1199function requireMultiselect(id, label) { 1200 return getMultiselectValues(id).length > 0 ? null : label; 1201} 1202 1203function validateStep(step) { 1204 const e = []; 1205 const push = (v) => { if (v) e.push(v); }; 1206 1207 if (step === 1) { 1208 push(requireInput("cnpj", "CNPJ")); 1209 push(requireInput("razao-social", "Razão Social")); 1210 push(requireInput("nome-fantasia", "Nome Fantasia")); 1211 push(requireInput("inscricao-estadual", "Inscrição Estadual")); 1212 push(requireInput("inscricao-municipal", "Inscrição Municipal")); 1213 push(requireInput("cep", "CEP")); 1214 push(requireSelect("uf-oficina", "UF")); 1215 push(requireInput("endereco", "Endereço")); 1216 push(requireInput("endereco-numero", "Número")); 1217 push(requireInput("bairro", "Bairro")); 1218 push(requireInput("cidade", "Cidade")); 1219 push(requireInput("proprietario", "Nome do Proprietário")); 1220 push(requireInput("telefone", "Telefone")); 1221 push(requireInput("email", "E-mail")); 1222 push(requireInput("orcamentista-nome", "Orçamentista - Nome")); 1223 push(requireInput("orcamentista-telefone", "Orçamentista - Telefone")); 1224 push(requireInput("orcamentista-email", "Orçamentista - E-mail")); 1225 push(requireInput("comprador-nome", "Comprador - Nome")); 1226 push(requireInput("comprador-telefone", "Comprador - Telefone")); 1227 push(requireInput("comprador-email", "Comprador - E-mail")); 1228 // Validações de formato 1229 const cnpj = getInputValue("cnpj"); 1230 if (cnpj && cnpj.replace(/\D/g, "").length !== 14) e.push("CNPJ inválido"); 1231 const email = getInputValue("email"); 1232 if (email && !email.includes("@")) e.push("E-mail inválido"); 1233 } 1234 1235 if (step === 2) { 1236 push(requireToggle("credenciada-seguradoras", "Credenciada em Seguradoras")); 1237 push(requireToggle("credenciada-associacoes", "Credenciada em Associações")); 1238 push(requireMultiselect("ms-seguradoras", "Empresas credenciadas")); 1239 push(requireInput("passagem-particulares", "Passagem veÃculos (Particulares)")); 1240 push(requireInput("passagem-credenciadas", "Passagem veÃculos (Credenciadas)")); 1241 push(requireToggle("possui-sistema-orcamento", "Possui Sistema de Orçamentação")); 1242 push(requireMultiselect("ms-sistema-orcamento", "Qual sistema de orçamentação")); 1243 push(requireToggle("possui-sistema-gestao", "Possui Sistema de Gestão")); 1244 if (getToggleValue("possui-sistema-gestao") === true) { 1245 push(requireMultiselect("ms-sistema-gestao", "Qual sistema de gestão")); 1246 } 1247 push(requireToggle("utiliza-plataforma-compra", "Utiliza plataforma de compra")); 1248 if (getToggleValue("utiliza-plataforma-compra") === true) { 1249 push(requireMultiselect("ms-plataformas-compra", "Quais plataformas de compra")); 1250 } 1251 push(requireToggle("aceita-fornecimento-pecas", "Aceita fornecimento de peças")); 1252 push(requireToggle("aceita-cartoes", "Aceita Cartões")); 1253 push(requireToggle("parcela-franquia", "Parcela a franquia")); 1254 if (getToggleValue("parcela-franquia") === true) { 1255 push(requireInput("quantas-parcelas", "Quantas parcelas")); 1256 } 1257 push(requireToggle("utiliza-antecipacao", "Utiliza antecipação de crédito")); 1258 push(requireMultiselect("ms-notas-fiscais", "Emite Notas Fiscais")); 1259 push(requireToggle("recebe-24horas", "Recebe 24 horas")); 1260 push(requireToggle("aceita-placa-clube", "Aceita placa do Clube")); 1261 push(requireMultiselect("ms-tipos-veiculos", "Tipos de veÃculos")); 1262 push(requireMultiselect("ms-marcas-veiculos", "Marcas de veÃculos")); 1263 push(requireMultiselect("ms-especializacoes", "Especializações")); 1264 } 1265 1266 if (step === 3) { 1267 push(requireToggle("possui-recepcao", "Possui recepção e sala de espera")); 1268 push(requireMultiselect("ms-banheiros", "Banheiros disponÃveis")); 1269 push(requireToggle("possui-local-pecas", "Possui local para peças")); 1270 push(requireToggle("possui-local-sucatas", "Possui local para sucatas")); 1271 push(requireToggle("possui-local-residuos", "Possui local para resÃduos")); 1272 } 1273 1274 if (step === 4) { 1275 push(requireToggle("mesa-estiramento", "Possui mesa de estiramento")); 1276 push(requireToggle("ciborg", "Possui Ciborg")); 1277 push(requireToggle("spotter", "Possui Spotter")); 1278 push(requireMultiselect("ms-tipos-solda", "Tipos de Solda")); 1279 push(requireToggle("mixer-tintas", "Possui Mixer de Tintas")); 1280 push(requireToggle("base-agua", "Utiliza Base d'água")); 1281 push(requireInput("marcas-tinta", "Marcas de Tinta")); 1282 push(requireMultiselect("ms-tipo-lixamento", "Tipo de Lixamento")); 1283 push(requireToggle("plano-aspirante", "Possui Plano Aspirante")); 1284 push(requireToggle("cabine-pintura", "Possui Cabine de Pintura")); 1285 push(requireToggle("cabine-aquecimento", "Cabine com Aquecimento")); 1286 push(requireToggle("scanner", "Possui Scanner")); 1287 if (getToggleValue("scanner") === true) { 1288 push(requireInput("scanner-marca", "Marca do scanner")); 1289 push(requireInput("scanner-modelo", "Modelo do scanner")); 1290 } 1291 push(requireToggle("ferramentas-plastico", "Ferramentas para plásticos")); 1292 if (getToggleValue("ferramentas-plastico") === true) { 1293 push(requireInput("marca-plastico", "Marca ferramentas plásticos")); 1294 } 1295 push(requireToggle("ferramentas-aluminio", "Ferramentas para alumÃnio")); 1296 if (getToggleValue("ferramentas-aluminio") === true) { 1297 push(requireInput("marca-aluminio", "Marca ferramentas alumÃnio")); 1298 } 1299 } 1300 1301 // Step 5 (Fotos) - sem validação obrigatória
1302 // Step 6 validado no submitForm() 1303 1304 return e; 1305} 1306 1307function validateFormData(data) { 1308 const errors = []; 1309 1310 // Valida todos os steps de 1 a 5 1311 for (let s = 1; s <= 5; s++) { 1312 validateStep(s).forEach((err) => errors.push(err)); 1313 } 1314 1315 // Step 6 - Colaboradores 1316 const cards = document.querySelectorAll(".colaborador-card"); 1317 cards.forEach((card, idx) => { 1318 const prefix = cards.length > 1 ? `Colaborador ${idx + 1} - ` : ""; 1319 const fields = card.querySelectorAll(".form-grid .field"); 1320 const getVal = (i) => fields[i]?.querySelector('input:not([type="hidden"])')?.value?.trim() || ""; 1321 const getSelVal = (i) => getCustomSelectValue(fields[i]?.querySelector(".custom-select")) || ""; 1322 1323 if (!getVal(0)) errors.push(`${prefix}Nome Completo`); 1324 if (!getSelVal(1)) errors.push(`${prefix}Cargo`); 1325 if (!getVal(2)) errors.push(`${prefix}CPF`); 1326 if (!getVal(3)) errors.push(`${prefix}Data de Nascimento`); 1327 if (!getVal(4)) errors.push(`${prefix}Endereço`); 1328 if (!getVal(5)) errors.push(`${prefix}Bairro`); 1329 if (!getVal(6)) errors.push(`${prefix}Cidade`); 1330 if (!getSelVal(7)) errors.push(`${prefix}UF`); 1331 if (!getVal(8)) errors.push(`${prefix}CEP`); 1332 if (!getVal(9)) errors.push(`${prefix}E-mail`); 1333 if (!getVal(10)) errors.push(`${prefix}Telefone`); 1334 }); 1335 1336 // Checkboxes 1337 if (!document.getElementById("checkbox-veracidade")?.checked) 1338 errors.push("Declaração de veracidade"); 1339 if (!document.getElementById("checkbox-optin")?.checked) 1340 errors.push("Aceite da PolÃtica de Privacidade"); 1341 1342 return { valid: errors.length === 0, errors }; 1343} 1344 1345// ========== MASCARAS ========== 1346function formatCNPJ(input) { 1347 let value = input.value.replace(/\D/g, ""); 1348 value = value.replace(/^(\d{2})(\d)/, "$1.$2"); 1349 value = value.replace(/^(\d{2})\.(\d{3})(\d)/, "$1.$2.$3"); 1350 value = value.replace(/\.(\d{3})(\d)/, ".$1/$2"); 1351 value = value.replace(/(\d{4})(\d)/, "$1-$2"); 1352 input.value = value; 1353} 1354 1355function formatCEP(input) { 1356 let value = input.value.replace(/\D/g, ""); 1357 value = value.replace(/^(\d{5})(\d)/, "$1-$2"); 1358 input.value = value; 1359} 1360 1361// ========== BUSCA DE CNPJ ========== 1362let cnpjLookupTimeout = null; 1363let lastCnpjLookup = ""; 1364let cnpjAbortController = null; 1365let cnpjStatusTimeout = null; 1366 1367function setCnpjLookupStatus(status) { 1368 const el = document.getElementById("cnpj-lookup-status"); 1369 if (!el) return; 1370 1371 clearTimeout(cnpjStatusTimeout); 1372 el.classList.remove("loading", "success", "error"); 1373 1374 if (!status || status === "idle") { 1375 el.textContent = ""; 1376 return; 1377 } 1378 1379 if (status === "loading") { 1380 el.classList.add("loading"); 1381 el.textContent = "â Buscando dados..."; 1382 return; 1383 } 1384 1385 if (status === "success") { 1386 el.classList.add("success"); 1387 el.textContent = "â Dados preenchidos"; 1388 cnpjStatusTimeout = setTimeout(() => setCnpjLookupStatus("idle"), 4000); 1389 return; 1390 } 1391 1392 el.classList.add("error"); 1393 el.textContent = "â CNPJ não encontrado"; 1394 cnpjStatusTimeout = setTimeout(() => setCnpjLookupStatus("idle"), 6000); 1395} 1396 1397function scheduleCnpjLookup(rawCnpj, immediate = false) { 1398 const cleanCnpj = (rawCnpj || "").replace(/\D/g, "").slice(0, 14); 1399 if (cleanCnpj.length !== 14) return; 1400 if (cleanCnpj === lastCnpjLookup) return; 1401 1402 clearTimeout(cnpjLookupTimeout); 1403 1404 if (immediate) { 1405 lookupCnpj(cleanCnpj); 1406 return; 1407 } 1408 1409 cnpjLookupTimeout = setTimeout(() => lookupCnpj(cleanCnpj), 300); 1410} 1411 1412function escapeRegExp(value) { 1413 return (value || "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); 1414} 1415 1416function buildLogradouroFromCnpj(data) { 1417 const tipo = (data?.descricao_tipo_de_logradouro || "").trim(); 1418 const logradouro = (data?.logradouro || "").trim(); 1419 const numero = (data?.numero || "").trim(); 1420 1421 let endereco = logradouro; 1422 if (tipo && logradouro) { 1423 const enderecoLower = logradouro.toLowerCase(); 1424 const tipoLower = tipo.toLowerCase(); 1425 if (!enderecoLower.startsWith(`${tipoLower} `)) { 1426 endereco = `${tipo} ${logradouro}`.trim(); 1427 } 1428 } 1429 1430 if (numero) { 1431 endereco = endereco 1432 .replace(new RegExp(`\\s*[,-]?\\s*${escapeRegExp(numero)}\\s*$`), "") 1433 .trim(); 1434 } 1435 1436 return endereco; 1437} 1438 1439function applyCnpjDataToForm(data) { 1440 if (!data) return; 1441 1442 if (data.razao_social) setInputValue("razao-social", data.razao_social); 1443 if (data.nome_fantasia) setInputValue("nome-fantasia", data.nome_fantasia); 1444 1445 if (data.cep) { 1446 setInputValue("cep", data.cep); 1447 const cepEl = document.getElementById("cep"); 1448 if (cepEl) formatCEP(cepEl); 1449 } 1450 1451 if (data.uf) setSelectValue("uf-oficina", data.uf); 1452 if (data.municipio) setInputValue("cidade", data.municipio); 1453 if (data.bairro) setInputValue("bairro", data.bairro); 1454 1455 const logradouro = buildLogradouroFromCnpj(data); 1456 if (logradouro) setInputValue("endereco", logradouro); 1457 if (data.numero) setInputValue("endereco-numero", data.numero); 1458 if (data.complemento) setInputValue("endereco-complemento", data.complemento); 1459 1460 if (!getInputValue("telefone") && data.ddd_telefone_1) { 1461 setInputValue("telefone", data.ddd_telefone_1); 1462 const telefoneEl = document.getElementById("telefone"); 1463 if (telefoneEl) formatPhone(telefoneEl); 1464 } 1465 1466 if (!getInputValue("email") && data.email) { 1467 setInputValue("email", data.email); 1468 } 1469} 1470 1471async function lookupCnpj(cleanCnpj) { 1472 if (cnpjAbortController) cnpjAbortController.abort(); 1473 cnpjAbortController = new AbortController(); 1474 1475 setCnpjLookupStatus("loading"); 1476 1477 try { 1478 const response = await fetch( 1479 `https://brasilapi.com.br/api/cnpj/v1/${cleanCnpj}`, 1480 { signal: cnpjAbortController.signal }, 1481 ); 1482 if (!response.ok) { 1483 setCnpjLookupStatus("error"); 1484 return; 1485 } 1486 1487 const data = await response.json(); 1488 if (!data?.cnpj) { 1489 setCnpjLookupStatus("error"); 1490 return; 1491 } 1492 1493 lastCnpjLookup = cleanCnpj; 1494 applyCnpjDataToForm(data); 1495 setCnpjLookupStatus("success"); 1496 } catch (error) { 1497 if (error?.name === "AbortError") return; 1498 console.warn("Erro ao buscar CNPJ:", error); 1499 setCnpjLookupStatus("error"); 1500 } 1501} 1502 1503// ========== BUSCA DE CEP ========== 1504let cepLookupTimeout = null; 1505let lastCepLookup = ""; 1506let cepAbortController = null; 1507 1508function scheduleCepLookup(rawCep, immediate = false) { 1509 const cleanCep = (rawCep || "").replace(/\D/g, "").slice(0, 8); 1510 if (cleanCep.length !== 8) return; 1511 if (cleanCep === lastCepLookup) return; 1512 1513 clearTimeout(cepLookupTimeout); 1514 1515 if (immediate) { 1516 lookupCep(cleanCep); 1517 return; 1518 } 1519 1520 cepLookupTimeout = setTimeout(() => lookupCep(cleanCep), 300); 1521} 1522 1523async function lookupCep(cleanCep) { 1524 if (cepAbortController) cepAbortController.abort(); 1525 cepAbortController = new AbortController(); 1526 1527 try { 1528 const response = await fetch(`https://viacep.com.br/ws/${cleanCep}/json/`, { 1529 signal: cepAbortController.signal, 1530 }); 1531 if (!response.ok) return; 1532 1533 const data = await response.json(); 1534 if (!data || data.erro) return; 1535 1536 lastCepLookup = cleanCep; 1537 1538 if (data.logradouro) setInputValue("endereco", data.logradouro); 1539 if (data.bairro) setInputValue("bairro", data.bairro); 1540 if (data.localidade) setInputValue("cidade", data.localidade); 1541 if (data.uf) setSelectValue("uf-oficina", data.uf); 1542 } catch (error) { 1543 if (error?.name === "AbortError") return; 1544 console.warn("Erro ao buscar CEP:", error); 1545 } 1546} 1547 1548function formatPhone(input) { 1549 const digits = (input.value || "").replace(/\D/g, "").slice(0, 11); 1550 1551 if (digits.length <= 2) { 1552 input.value = digits; 1553 return; 1554 } 1555 1556 const ddd = digits.slice(0, 2); 1557 const rest = digits.slice(2); 1558 1559 if (rest.length <= 4) { 1560 input.value = `(${ddd}) ${rest}`; 1561 return; 1562 } 1563 1564 if (rest.length <= 8) { 1565 input.value = `(${ddd}) ${rest.slice(0, 4)}-${rest.slice(4)}`; 1566 return; 1567 } 1568 1569 input.value = `(${ddd}) ${rest.slice(0, 5)}-${rest.slice(5)}`; 1570} 1571 1572function formatCPF(input) { 1573 let value = input.value.replace(/\D/g, ""); 1574 value = value.replace(/(\d{3})(\d)/, "$1.$2"); 1575 value = value.replace(/(\d{3})(\d)/, "$1.$2"); 1576 value = value.replace(/(\d{3})(\d{1,2})$/, "$1-$2"); 1577 input.value = value; 1578} 1579 1580// ========== MULTISELECT COMPONENT ========== 1581 1582// Inicializa um multiselect com base na configuracao 1583function initMultiselect(id) { 1584 const config = MULTISELECT_OPTIONS[id]; 1585 if (!config) return; 1586 1587 const container = document.getElementById(id); 1588 if (!container) return; 1589 1590 // Define atributo de custom 1591 if (config.allowCustom) { 1592 container.dataset.allowCustom = "true"; 1593 } 1594 1595 // Gera HTML do multiselect 1596 container.innerHTML = ` 1597 <div class="multiselect-trigger" onclick="toggleMultiselect('${id}')">
1598 <span class="multiselect-placeholder">${config.placeholder}</span> 1599 </div> 1600 <div class="multiselect-dropdown"> 1601 <div class="multiselect-search"> 1602 <input type="text" placeholder="Buscar..." oninput="filterMultiselect('${id}', this.value)"> 1603 </div> 1604 <div class="multiselect-options"> 1605 ${config.options 1606 .map( 1607 (opt) => ` 1608 <div class="multiselect-option${opt.selected ? " selected" : ""}" 1609 data-value="${opt.value}" 1610 onclick="selectOption('${id}', this)"> 1611 <div class="multiselect-option-checkbox"></div> 1612 <span>${opt.label}</span> 1613 </div> 1614 `, 1615 ) 1616 .join("")} 1617 </div> 1618 ${ 1619 config.allowCustom 1620 ? ` 1621 <div class="multiselect-add-custom hidden" onclick="addCustomOption('${id}')"> 1622 <i class="pi pi-plus"></i> 1623 <span>Adicionar "<span class="custom-value"></span>"</span> 1624 </div> 1625 ` 1626 : "" 1627 } 1628 </div> 1629 `; 1630 1631 // Atualiza trigger se houver opcoes pre-selecionadas 1632 updateMultiselectTrigger(id); 1633} 1634 1635// Toggle dropdown do multiselect 1636function toggleMultiselect(id) { 1637 const multiselect = document.getElementById(id); 1638 const dropdown = multiselect.querySelector(".multiselect-dropdown"); 1639 const trigger = multiselect.querySelector(".multiselect-trigger"); 1640 const isOpen = dropdown.classList.contains("open"); 1641 1642 // Fecha todos os outros dropdowns 1643 document.querySelectorAll(".multiselect-dropdown.open").forEach((d) => { 1644 d.classList.remove("open"); 1645 d.closest(".multiselect") 1646 .querySelector(".multiselect-trigger") 1647 .classList.remove("active"); 1648 }); 1649 1650 if (!isOpen) { 1651 dropdown.classList.add("open"); 1652 trigger.classList.add("active"); 1653 const searchInput = dropdown.querySelector(".multiselect-search input"); 1654 if (searchInput) { 1655 searchInput.value = ""; 1656 searchInput.focus(); 1657 filterMultiselect(id, ""); 1658 } 1659 } 1660} 1661 1662// Seleciona/deseleciona uma opcao 1663function selectOption(id, optionEl) { 1664 optionEl.classList.toggle("selected"); 1665 updateMultiselectTrigger(id); 1666 event.stopPropagation(); 1667} 1668 1669// Atualiza o trigger com os chips selecionados 1670function updateMultiselectTrigger(id) { 1671 const multiselect = document.getElementById(id); 1672 if (!multiselect) return; 1673 1674 const trigger = multiselect.querySelector(".multiselect-trigger"); 1675 if (!trigger) return; 1676 1677 const selectedOptions = multiselect.querySelectorAll( 1678 ".multiselect-option.selected", 1679 ); 1680 let placeholder = trigger.querySelector(".multiselect-placeholder"); 1681 1682 // Remove chips existentes 1683 trigger 1684 .querySelectorAll(".multiselect-chip") 1685 .forEach((chip) => chip.remove()); 1686 1687 if (selectedOptions.length === 0) { 1688 if (!placeholder) { 1689 const config = MULTISELECT_OPTIONS[id]; 1690 const span = document.createElement("span"); 1691 span.className = "multiselect-placeholder"; 1692 span.textContent = config ? config.placeholder : "Selecione"; 1693 trigger.insertBefore(span, trigger.firstChild); 1694 } else { 1695 placeholder.style.display = "inline"; 1696 } 1697 } else { 1698 if (placeholder) placeholder.style.display = "none"; 1699 1700 selectedOptions.forEach((option) => { 1701 const chip = document.createElement("div"); 1702 chip.className = "multiselect-chip"; 1703 if (option.dataset.custom === "true") { 1704 chip.classList.add("custom"); 1705 } 1706 chip.innerHTML = ` 1707 <span>${option.querySelector("span").textContent}</span> 1708 <i class="pi pi-times multiselect-chip-remove" onclick="removeChip('${id}', '${option.dataset.value}', event)"></i> 1709 `; 1710 trigger.insertBefore(chip, trigger.lastChild); 1711 }); 1712 } 1713} 1714 1715// Remove um chip 1716function removeChip(id, value, event) { 1717 event.stopPropagation(); 1718 const multiselect = document.getElementById(id); 1719 const option = multiselect.querySelector( 1720 `.multiselect-option[data-value="${value}"]`, 1721 ); 1722 if (option) { 1723 option.classList.remove("selected"); 1724 if (option.dataset.custom === "true") { 1725 option.remove(); 1726 } 1727 } 1728 updateMultiselectTrigger(id); 1729} 1730 1731// Filtra opcoes do multiselect 1732function filterMultiselect(id, searchTerm) { 1733 const multiselect = document.getElementById(id); 1734 const options = multiselect.querySelectorAll(".multiselect-option"); 1735 const addCustomBtn = multiselect.querySelector(".multiselect-add-custom"); 1736 const allowCustom = multiselect.dataset.allowCustom === "true"; 1737 const normalizedSearch = searchTerm.toLowerCase().trim(); 1738 1739 let hasVisibleOption = false;
1740 let hasExactMatch = false; 1741 1742 options.forEach((option) => { 1743 const label = option.querySelector("span").textContent.toLowerCase(); 1744 const matches = label.includes(normalizedSearch); 1745 1746 if (matches) { 1747 option.classList.remove("hidden"); 1748 hasVisibleOption = true; 1749 if (label === normalizedSearch) { 1750 hasExactMatch = true; 1751 } 1752 } else { 1753 option.classList.add("hidden"); 1754 } 1755 }); 1756 1757 // Mostra/esconde botao de adicionar customizado 1758 if (allowCustom && addCustomBtn) { 1759 if (normalizedSearch && !hasExactMatch) { 1760 addCustomBtn.classList.remove("hidden"); 1761 addCustomBtn.querySelector(".custom-value").textContent = searchTerm; 1762 } else { 1763 addCustomBtn.classList.add("hidden"); 1764 } 1765 } 1766} 1767 1768// Adiciona uma opcao customizada 1769function addCustomOption(id) { 1770 const multiselect = document.getElementById(id); 1771 const searchInput = multiselect.querySelector(".multiselect-search input"); 1772 const optionsContainer = multiselect.querySelector(".multiselect-options"); 1773 const addCustomBtn = multiselect.querySelector(".multiselect-add-custom"); 1774 const customValue = searchInput.value.trim(); 1775 1776 if (!customValue) return; 1777 1778 // Cria nova opcao 1779 const newOption = document.createElement("div"); 1780 const valueSlug = customValue 1781 .toLowerCase() 1782 .replace(/\s+/g, "-") 1783 .replace(/[^a-z0-9-]/g, ""); 1784 newOption.className = "multiselect-option selected"; 1785 newOption.dataset.value = "custom-" + valueSlug + "-" + Date.now(); 1786 newOption.dataset.custom = "true"; 1787 newOption.onclick = function () { 1788 selectOption(id, this); 1789 }; 1790 newOption.innerHTML = ` 1791 <div class="multiselect-option-checkbox"></div> 1792 <span>${customValue}</span> 1793 `; 1794 1795 optionsContainer.appendChild(newOption); 1796 1797 // Limpa busca e atualiza 1798 searchInput.value = ""; 1799 filterMultiselect(id, ""); 1800 addCustomBtn.classList.add("hidden"); 1801 updateMultiselectTrigger(id); 1802} 1803 1804// ========== CUSTOM SELECT (SINGLE) COMPONENT ========== 1805 1806// Inicializa um custom select 1807function initCustomSelect(element, configKey) { 1808 const config = CUSTOM_SELECT_OPTIONS[configKey]; 1809 if (!config) return; 1810 1811 // Cria o container 1812 const container = document.createElement("div"); 1813 container.className = "custom-select"; 1814 container.dataset.config = configKey; 1815 if (element.id) container.id = element.id; 1816 1817 // Pega o valor atual do select nativo se houver 1818 let currentValue = element.value; 1819 let currentLabel = ""; 1820 if (currentValue) { 1821 const opt = config.options.find((o) => o.value === currentValue); 1822 if (opt) currentLabel = opt.label; 1823 } 1824 1825 // Gera HTML 1826 container.innerHTML = ` 1827 <div class="custom-select-trigger" onclick="toggleCustomSelect(this.parentElement)"> 1828 ${currentValue ? `<span class="custom-select-value">${currentLabel}</span>` : `<span class="custom-select-placeholder">${config.placeholder}</span>`} 1829 </div> 1830 <div class="custom-select-dropdown"> 1831 ${ 1832 config.searchable 1833 ? ` 1834 <div class="custom-select-search"> 1835 <input type="text" placeholder="Buscar..." oninput="filterCustomSelect(this)"> 1836 </div> 1837 ` 1838 : "" 1839 } 1840 <div class="custom-select-options"> 1841 ${config.options 1842 .map( 1843 (opt) => ` 1844 <div class="custom-select-option${opt.value === currentValue ? " selected" : ""}" 1845 data-value="${opt.value}" 1846 onclick="selectCustomOption(this)"> 1847 ${opt.label} 1848 </div> 1849 `, 1850 ) 1851 .join("")} 1852 </div> 1853 </div> 1854 <input type="hidden" name="${element.name || ""}" value="${currentValue || ""}"> 1855 `; 1856 1857 // Substitui o select nativo 1858 element.replaceWith(container); 1859 return container; 1860} 1861 1862// Toggle dropdown do custom select 1863function toggleCustomSelect(container) { 1864 const dropdown = container.querySelector(".custom-select-dropdown"); 1865 const trigger = container.querySelector(".custom-select-trigger"); 1866 const isOpen = dropdown.classList.contains("open"); 1867 1868 // Fecha todos os outros dropdowns (multiselect e custom-select) 1869 document 1870 .querySelectorAll( 1871 ".multiselect-dropdown.open, .custom-select-dropdown.open", 1872 ) 1873 .forEach((d) => { 1874 d.classList.remove("open"); 1875 const parentTrigger = d.parentElement.querySelector( 1876 ".multiselect-trigger, .custom-select-trigger", 1877 ); 1878 if (parentTrigger) parentTrigger.classList.remove("active"); 1879 }); 1880 1881 if (!isOpen) { 1882 dropdown.classList.add("open"); 1883 trigger.classList.add("active"); 1884 const searchInput = dropdown.querySelector(".custom-select-search input"); 1885 if (searchInput) { 1886 searchInput.value = ""; 1887 searchInput.focus(); 1888 filterCustomSelect(searchInput); 1889 } 1890 } 1891} 1892 1893// Seleciona uma opção 1894function selectCustomOption(optionEl) { 1895 const container = optionEl.closest(".custom-select"); 1896 const dropdown = container.querySelector(".custom-select-dropdown"); 1897 const trigger = container.querySelector(".custom-select-trigger"); 1898 const hiddenInput = container.querySelector('input[type="hidden"]'); 1899 1900 // Remove selected de outras opções 1901 container.querySelectorAll(".custom-select-option").forEach((opt) => { 1902 opt.classList.remove("selected"); 1903 }); 1904 1905 // Marca esta como selecionada 1906 optionEl.classList.add("selected"); 1907 1908 // Atualiza o trigger 1909 trigger.innerHTML = `<span class="custom-select-value">${optionEl.textContent.trim()}</span>`;
1910 1911 // Atualiza o input hidden 1912 if (hiddenInput) { 1913 hiddenInput.value = optionEl.dataset.value; 1914 } 1915 1916 // Fecha o dropdown 1917 dropdown.classList.remove("open"); 1918 trigger.classList.remove("active"); 1919 1920 event.stopPropagation(); 1921} 1922 1923// Filtra opções do custom select 1924function filterCustomSelect(searchInput) { 1925 const container = searchInput.closest(".custom-select"); 1926 const options = container.querySelectorAll(".custom-select-option"); 1927 const searchTerm = searchInput.value.toLowerCase().trim(); 1928 1929 options.forEach((option) => { 1930 const label = option.textContent.toLowerCase(); 1931 if (label.includes(searchTerm)) { 1932 option.classList.remove("hidden"); 1933 } else { 1934 option.classList.add("hidden"); 1935 } 1936 }); 1937} 1938 1939// Inicializa todos os selects nativos que devem ser convertidos 1940function initAllCustomSelects() { 1941 // Selects de UF 1942 document.querySelectorAll("select.input-select").forEach((select) => { 1943 const label = 1944 select 1945 .closest(".field") 1946 ?.querySelector("label") 1947 ?.textContent?.toLowerCase() || ""; 1948 const options = Array.from(select.options); 1949 1950 // Detecta o tipo de select pelo label ou opções 1951 if ( 1952 label.includes("uf") || 1953 label.includes("estado") || 1954 (options.length > 2 && 1955 options.some((o) => o.value === "SP" || o.text === "SP")) 1956 ) { 1957 initCustomSelect(select, "uf"); 1958 } else if (label.includes("cargo")) { 1959 initCustomSelect(select, "cargo"); 1960 } else if (label.includes("parentesco")) { 1961 initCustomSelect(select, "parentesco"); 1962 } 1963 }); 1964} 1965 1966// ========== RASCUNHO (AUTO-SAVE + RESTORE) ========== 1967const DRAFT_PREFIX = "cilia_oficina_draft_"; 1968const LEAD_PARCIAL_URL = "https://script.google.com/macros/s/AKfycbwYov4PINuqz0RKkCpJ-kdMVOCCpBiVxgGQAgQRNnLVc-z2BPQoAI3jw1-XRdi7tsoaQA/exec"; 1969 1970let autoSaveTimeout = null; 1971let formSubmitted = false; 1972 1973function getDraftKey() { 1974 const cnpj = (getInputValue("cnpj") || "").replace(/\D/g, ""); 1975 return cnpj.length === 14 ? DRAFT_PREFIX + cnpj : null; 1976} 1977 1978function collectDraftData() { 1979 // Coleta tudo exceto fotos (muito grandes para localStorage) 1980 return { 1981 currentStep, 1982 timestamp: new Date().toISOString(), 1983 dadosCadastrais: collectDadosCadastrais(), 1984 operacao: collectOperacao(), 1985 estrutura: collectEstrutura(), 1986 ferramental: collectFerramental(), 1987 colaboradores: collectColaboradores(), 1988 }; 1989} 1990 1991function scheduleDraftSave() { 1992 clearTimeout(autoSaveTimeout); 1993 autoSaveTimeout = setTimeout(saveDraft, 1000); 1994} 1995 1996function saveDraft() { 1997 const key = getDraftKey(); 1998 if (!key) return; 1999 try { 2000 localStorage.setItem(key, JSON.stringify(collectDraftData())); 2001 } catch (e) { 2002 console.warn("Erro ao salvar rascunho:", e); 2003 } 2004} 2005 2006function loadDraft(cnpj) { 2007 const clean = (cnpj || "").replace(/\D/g, ""); 2008 if (clean.length !== 14) return null; 2009 try { 2010 const raw = localStorage.getItem(DRAFT_PREFIX + clean); 2011 return raw ? JSON.parse(raw) : null; 2012 } catch (e) { 2013 return null; 2014 } 2015} 2016 2017function clearDraft() { 2018 const key = getDraftKey(); 2019 if (key) localStorage.removeItem(key); 2020} 2021 2022function restoreDraft(data) { 2023 if (!data) return; 2024 const d = data.dadosCadastrais; 2025 if (d) { 2026 // Não sobrescreve CNPJ (já está preenchido) 2027 setInputValue("razao-social", d.razaoSocial); 2028 setInputValue("nome-fantasia", d.nomeFantasia); 2029 setInputValue("inscricao-estadual", d.inscricaoEstadual); 2030 setInputValue("inscricao-municipal", d.inscricaoMunicipal); 2031 setInputValue("cep", d.cep); 2032 setSelectValue("uf-oficina", d.uf); 2033 setInputValue("endereco", d.logradouro); 2034 setInputValue("endereco-numero", d.numero); 2035 setInputValue("endereco-complemento", d.complemento); 2036 setInputValue("bairro", d.bairro); 2037 setInputValue("cidade", d.cidade); 2038 setInputValue("proprietario", d.proprietario); 2039 setInputValue("telefone", d.telefone); 2040 setInputValue("email", d.email); 2041 if (d.orcamentista) { 2042 setInputValue("orcamentista-nome", d.orcamentista.nome); 2043 setInputValue("orcamentista-telefone", d.orcamentista.telefone); 2044 setInputValue("orcamentista-email", d.orcamentista.email); 2045 } 2046 if (d.compradorPecas) { 2047 setInputValue("comprador-nome", d.compradorPecas.nome); 2048 setInputValue("comprador-telefone", d.compradorPecas.telefone); 2049 setInputValue("comprador-email", d.compradorPecas.email); 2050 } 2051 } 2052 2053 const o = data.operacao; 2054 if (o) { 2055 setToggleValue("credenciada-seguradoras", o.credenciadaSeguradoras); 2056 setToggleValue("credenciada-associacoes", o.credenciadaAssociacoes); 2057 setMultiselectValues("ms-seguradoras", o.seguradoras || []); 2058 setInputValue("passagem-particulares", o.passagemParticulares || ""); 2059 setInputValue("passagem-credenciadas", o.passagemCredenciadas || ""); 2060 setToggleValue("possui-sistema-orcamento", o.possuiSistemaOrcamento); 2061 setMultiselectValues("ms-sistema-orcamento", o.sistemasOrcamento || []); 2062 setToggleValue("possui-sistema-gestao", o.possuiSistemaGestao); 2063 setMultiselectValues("ms-sistema-gestao", o.sistemasGestao || []); 2064 setToggleValue("utiliza-plataforma-compra", o.utilizaPlataformaCompra); 2065 setMultiselectValues("ms-plataformas-compra", o.plataformasCompra || []); 2066 setToggleValue("aceita-fornecimento-pecas", o.aceitaFornecimentoPecas); 2067 setToggleValue("aceita-cartoes", o.aceitaCartoes); 2068 setToggleValue("parcela-franquia", o.parcelaFranquia); 2069 setInputValue("quantas-parcelas", o.quantasParcelas || ""); 2070 setToggleValue("utiliza-antecipacao", o.utilizaAntecipacao); 2071 setMultiselectValues("ms-notas-fiscais", o.tiposNotaFiscal || []); 2072 setToggleValue("recebe-24horas", o.recebe24horas); 2073 setToggleValue("aceita-placa-clube", o.aceitaPlacaClube); 2074 setMultiselectValues("ms-tipos-veiculos", o.tiposVeiculos || []); 2075 setMultiselectValues("ms-marcas-veiculos", o.marcasVeiculos || []); 2076 setMultiselectValues("ms-especializacoes", o.especializacoes || []); 2077 } 2078 2079 const infra = data.estrutura?.infraestrutura; 2080 if (infra) { 2081 setToggleValue("possui-recepcao", infra.possuiRecepcao); 2082 setMultiselectValues("ms-banheiros", infra.banheiros || []); 2083 setToggleValue("possui-local-pecas", infra.localPecas); 2084 setToggleValue("possui-local-sucatas", infra.localSucatas); 2085 setToggleValue("possui-local-residuos", infra.localResiduos); 2086 } 2087 2088 const f = data.ferramental; 2089 if (f) { 2090 setToggleValue("mesa-estiramento", f.funilaria?.mesaEstiramento); 2091 setToggleValue("ciborg", f.funilaria?.ciborg); 2092 setToggleValue("spotter", f.funilaria?.spotter); 2093 setMultiselectValues("ms-tipos-solda", f.funilaria?.tiposSolda || []); 2094 setToggleValue("mixer-tintas", f.pintura?.mixerTintas); 2095 setToggleValue("base-agua", f.pintura?.baseAgua); 2096 setInputValue("marcas-tinta", f.pintura?.marcasTinta); 2097 setMultiselectValues("ms-tipo-lixamento", f.pintura?.tipoLixamento || []); 2098 setToggleValue("plano-aspirante", f.pintura?.planoAspirante); 2099 setToggleValue("cabine-pintura", f.pintura?.cabinePintura); 2100 setToggleValue("cabine-aquecimento", f.pintura?.cabineAquecimento); 2101 setToggleValue("scanner", f.diagnostico?.scanner); 2102 setInputValue("scanner-marca", f.diagnostico?.marcaScanner); 2103 setInputValue("scanner-modelo", f.diagnostico?.modeloScanner); 2104 setToggleValue("ferramentas-plastico", f.reparacaoEspecial?.ferramentasPlastico); 2105 setInputValue("marca-plastico", f.reparacaoEspecial?.marcaPlastico); 2106 setToggleValue("ferramentas-aluminio", f.reparacaoEspecial?.ferramentasAluminio); 2107 setInputValue("marca-aluminio", f.reparacaoEspecial?.marcaAluminio); 2108 } 2109 2110 // Profissionais e suporte - restaura inputs numéricos nas tabelas 2111 const prof = data.estrutura?.profissionais; 2112 if (prof) { 2113 const rows = document.querySelectorAll("#step3-content .professionals-table:first-of-type tbody tr"); 2114 const funcoes = ["orcamentista","compradorPecas","funileiro","preparador","pintor","colorista","montador","estoquista","mecanico","polidor","martelinho"]; 2115 rows.forEach((row, i) => { 2116 const fn = funcoes[i]; 2117 if (fn && prof[fn]) { 2118 const inputs = row.querySelectorAll('input[type="number"]');
2119 if (inputs[0]) inputs[0].value = prof[fn].qtdProprio || 0; 2120 if (inputs[1]) inputs[1].value = prof[fn].qtdTerceirizado || 0; 2121 } 2122 }); 2123 } 2124 const sup = data.estrutura?.suporte; 2125 if (sup) { 2126 const rows = document.querySelectorAll("#step3-content .professionals-table:last-of-type tbody tr"); 2127 const funcoes = ["recepcao","financeiro","administrativo"]; 2128 rows.forEach((row, i) => { 2129 const fn = funcoes[i]; 2130 if (fn && sup[fn]) { 2131 const inputs = row.querySelectorAll('input[type="number"]'); 2132 if (inputs[0]) inputs[0].value = sup[fn].qtdProprio || 0; 2133 if (inputs[1]) inputs[1].value = sup[fn].qtdTerceirizado || 0; 2134 } 2135 }); 2136 } 2137} 2138 2139// ========== LEAD PARCIAL (ABANDONO) ========== 2140function sendPartialLead() { 2141 if (formSubmitted) return; 2142 const cnpj = (getInputValue("cnpj") || "").replace(/\D/g, ""); 2143 const telefone = getInputValue("telefone"); 2144 const email = getInputValue("email"); 2145 if (!cnpj && !telefone && !email) return; 2146 2147 if (LEAD_API_URL && LEAD_API_KEY) { 2148 // Modo API Gateway (AWS) 2149 const formData = collectFormData(); 2150 formData.cnpj = formData.dadosCadastrais?.cnpj || ""; 2151 formData.status = "Pendente"; 2152 formData.etapaAbandono = currentStep; 2153 fetch(LEAD_API_URL, { 2154 method: "POST", 2155 headers: { "Content-Type": "application/json", "x-api-key": LEAD_API_KEY }, 2156 body: JSON.stringify(formData), 2157 keepalive: true, 2158 }).catch(() => {}); 2159 } else if (LEAD_PARCIAL_URL) { 2160 // Modo Google Apps Script (legado) 2161 const formData = collectFormData(); 2162 formData.status = "Pendente"; 2163 formData.etapaAbandono = currentStep; 2164 formData.timestamp = new Date().toISOString(); 2165 const blob = new Blob([JSON.stringify(formData)], { type: "application/json" }); 2166 navigator.sendBeacon(LEAD_PARCIAL_URL, blob); 2167 } 2168} 2169 2170// ========== INICIALIZACAO ========== 2171function init() { 2172 // Inicializa todos os multiselects 2173 Object.keys(MULTISELECT_OPTIONS).forEach((id) => { 2174 initMultiselect(id); 2175 }); 2176 2177 // Inicializa todos os custom selects 2178 initAllCustomSelects(); 2179 2180 bindAlertModalInteractions(document.getElementById("alert-modal")); 2181 2182 // Fecha dropdown ao clicar fora 2183 document.addEventListener("click", function (e) { 2184 // Fecha multiselects 2185 if (!e.target.closest(".multiselect")) { 2186 document 2187 .querySelectorAll(".multiselect-dropdown.open") 2188 .forEach((dropdown) => { 2189 dropdown.classList.remove("open"); 2190 dropdown 2191 .closest(".multiselect") 2192 .querySelector(".multiselect-trigger") 2193 .classList.remove("active"); 2194 }); 2195 } 2196 // Fecha custom-selects 2197 if (!e.target.closest(".custom-select")) { 2198 document 2199 .querySelectorAll(".custom-select-dropdown.open") 2200 .forEach((dropdown) => { 2201 dropdown.classList.remove("open"); 2202 dropdown 2203 .closest(".custom-select") 2204 .querySelector(".custom-select-trigger") 2205 .classList.remove("active"); 2206 }); 2207 } 2208 }); 2209 2210 // Previne que cliques dentro do dropdown fechem ele 2211 document 2212 .querySelectorAll(".multiselect-dropdown, .custom-select-dropdown") 2213 .forEach((dropdown) => { 2214 dropdown.addEventListener("click", function (e) { 2215 e.stopPropagation(); 2216 }); 2217 }); 2218 2219 // Aplica mascaras 2220 const cnpjInput = document.getElementById("cnpj"); 2221 const cepInput = document.getElementById("cep"); 2222 const telefoneInput = document.getElementById("telefone"); 2223 2224 if (cnpjInput) { 2225 cnpjInput.addEventListener("input", (e) => { 2226 formatCNPJ(e.target); 2227 scheduleCnpjLookup(e.target.value); 2228 if (e.target.value.replace(/\D/g, "").length < 14) { 2229 setCnpjLookupStatus("idle"); 2230 } 2231 // Tenta restaurar rascunho ao completar CNPJ 2232 const clean = e.target.value.replace(/\D/g, ""); 2233 if (clean.length === 14) { 2234 const draft = loadDraft(clean); 2235 if (draft) { 2236 restoreDraft(draft); 2237 setCnpjLookupStatus("success"); 2238 const statusEl = document.getElementById("cnpj-lookup-status"); 2239 if (statusEl) statusEl.textContent = "â Rascunho restaurado"; 2240 } 2241 } 2242 }); 2243 cnpjInput.addEventListener("blur", (e) => 2244 scheduleCnpjLookup(e.target.value, true), 2245 ); 2246 scheduleCnpjLookup(cnpjInput.value, true); 2247 } 2248 if (cepInput) { 2249 cepInput.addEventListener("input", (e) => { 2250 formatCEP(e.target); 2251 scheduleCepLookup(e.target.value); 2252 }); 2253 cepInput.addEventListener("blur", (e) => 2254 scheduleCepLookup(e.target.value, true), 2255 ); 2256 scheduleCepLookup(cepInput.value, true); 2257 } 2258 if (telefoneInput) 2259 telefoneInput.addEventListener("input", (e) => formatPhone(e.target)); 2260 2261 // Atualiza info mobile inicial 2262 updateMobileProgressInfo(); 2263 2264 // Auto-save: escuta mudanças em todos os campos do formulário 2265 document.addEventListener("input", scheduleDraftSave); 2266 document.addEventListener("click", (e) => { 2267 if (e.target.closest(".toggle-btn") || e.target.closest(".multiselect-option")) { 2268 scheduleDraftSave(); 2269 } 2270 }); 2271
2272 // Lead parcial: envia dados ao sair da página 2273 window.addEventListener("beforeunload", sendPartialLead); 2274 document.addEventListener("visibilitychange", () => { 2275 if (document.visibilityState === "hidden") sendPartialLead(); 2276 }); 2277} 2278 2279document.addEventListener("DOMContentLoaded", init); 2280init()
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.