1// ============================================= 2// admin.js â Panel Administrativo 3// ============================================= 4 5let campaigns = []; 6let menuItems = []; 7let editCampaignId = null; 8let editMenuItemId = null; 9let uploadedImgUrl = null; 10 11// ââ AUTH ââââââââââââââââââââââââââââââââââââââ 12document.addEventListener('DOMContentLoaded', () => { 13 if (localStorage.getItem('lbd_admin') === 'true') showAdmin(); 14 15 document.getElementById('loginPassword') 16 .addEventListener('keydown', e => { if (e.key === 'Enter') doLogin(); }); 17}); 18 19function doLogin() { 20 const val = document.getElementById('loginPassword').value; 21 if (val === ADMIN_PASSWORD) { 22 localStorage.setItem('lbd_admin', 'true'); 23 showAdmin(); 24 } else { 25 const err = document.getElementById('loginError'); 26 err.textContent = 'â Contraseña incorrecta'; 27 document.getElementById('loginPassword').value = ''; 28 setTimeout(() => err.textContent = '', 3000); 29 } 30} 31 32function showAdmin() { 33 document.getElementById('loginScreen').style.display = 'none'; 34 document.getElementById('adminPanel').style.display = 'flex'; 35 loadCampaigns(); 36 loadMenuItems(); 37 loadConfigForm(); 38 loadNosotros(); 39 loadPopupTab(); 40} 41 42function doLogout() { 43 localStorage.removeItem('lbd_admin'); 44 location.reload(); 45} 46 47// ââ TABS ââââââââââââââââââââââââââââââââââââââ 48function switchTab(tab, btn) { 49 document.querySelectorAll('.nav-item').forEach(b => b.classList.remove('active')); 50 document.querySelectorAll('.tab-content').forEach(t => t.classList.remove('active')); 51 btn.classList.add('active'); 52 document.getElementById(`tab-${tab}`).classList.add('active'); 53} 54 55// ââ TOAST âââââââââââââââââââââââââââââââââââââ 56function toast(msg, type = 'success') { 57 const el = document.getElementById('toast'); 58 el.textContent = msg; 59 el.className = `toast ${type} show`; 60 setTimeout(() => el.className = 'toast', 3200); 61} 62 63// âââââââââââââââââââââââââââââââââââââââââââââââ 64// CAMPAÃAS 65// âââââââââââââââââââââââââââââââââââââââââââââââ 66async function loadCampaigns() { 67 const { data, error } = await _supabase 68 .from('campaigns').select('*').order('created_at', { ascending: false }); 69 if (error) { toast('Error cargando campañas', 'error'); return; } 70 campaigns = data || []; 71 renderCampaigns(); 72} 73 74function renderCampaigns() { 75 const grid = document.getElementById('campaignsGrid'); 76 if (!campaigns.length) { 77 grid.innerHTML = '<div class="empty-state">No hay campañas todavÃa.<br>¡Crea la primera!</div>'; 78 return; 79 } 80 grid.innerHTML = campaigns.map(c => ` 81 <div class="campaign-card ${c.is_active ? 'is-active' : ''}"> 82 <div class="camp-img" style="${c.hero_image_url 83 ? `background-image:url('${c.hero_image_url}');background-size:cover;background-position:center` 84 : ''}"> 85 ${!c.hero_image_url ? '<span style="font-size:2.5rem">ð¨</span>' : ''} 86 ${c.is_active ? '<div class="live-badge">ð¢ EN VIVO</div>' : ''} 87 </div> 88 <div class="camp-info"> 89 <div class="camp-name">${c.name}</div> 90 <div class="camp-title">"${c.hero_title}"</div> 91 <div class="camp-sub">${c.hero_subtitle || ''}</div> 92 </div> 93 <div class="camp-actions"> 94 ${!c.is_active 95 ? `<button class="btn-activate" onclick="activateCampaign('${c.id}')">â¡ Activar</button>` 96 : `<span class="active-label">En vivo ahora</span>`} 97 <button class="btn-edit-card" onclick="openCampaignModal('${c.id}')">âï¸ Editar</button> 98 <button class="btn-delete-card" onclick="deleteCampaign('${c.id}')">ð</button> 99 </div> 100 </div> 101 `).join(''); 102} 103 104function openCampaignModal(id = null) { 105 editCampaignId = id; 106 uploadedImgUrl = null; 107 108 // Reset fields 109 ['campaignId','campaignName','campaignTitle','campaignSubtitle','campaignCta','campaignImageUrl'] 110 .forEach(f => document.getElementById(f).value = ''); 111 document.getElementById('campaignActive').checked = false;
112 document.getElementById('imageInput').value = ''; 113 document.getElementById('imgPreviewWrap').style.display = 'none'; 114 document.getElementById('uploadArea').style.display = 'flex'; 115 116 if (id) { 117 const c = campaigns.find(x => x.id === id); 118 if (!c) return; 119 document.getElementById('campaignModalTitle').textContent = 'Editar Campaña'; 120 document.getElementById('campaignId').value = c.id; 121 document.getElementById('campaignName').value = c.name; 122 document.getElementById('campaignTitle').value = c.hero_title; 123 document.getElementById('campaignSubtitle').value = c.hero_subtitle || ''; 124 document.getElementById('campaignCta').value = c.hero_cta_text || ''; 125 document.getElementById('campaignActive').checked = c.is_active; 126 if (c.hero_image_url) { 127 document.getElementById('campaignImageUrl').value = c.hero_image_url; 128 setImgPreview(c.hero_image_url); 129 } 130 } else { 131 document.getElementById('campaignModalTitle').textContent = 'Nueva Campaña'; 132 } 133 document.getElementById('campaignModal').classList.add('open'); 134} 135 136function closeCampaignModal(e) { 137 if (e && e.target !== e.currentTarget) return; 138 document.getElementById('campaignModal').classList.remove('open'); 139 editCampaignId = uploadedImgUrl = null; 140} 141 142async function previewImage(input) { 143 const file = input.files[0]; 144 if (!file) return; 145 146 // Preview local 147 const reader = new FileReader(); 148 reader.onload = ev => setImgPreview(ev.target.result); 149 reader.readAsDataURL(file); 150 151 // Upload a Supabase Storage 152 toast('Subiendo imagenâ¦', 'info'); 153 const fname = `campaign-${Date.now()}-${file.name.replace(/[^a-z0-9.]/gi, '-').toLowerCase()}`; 154 const { error } = await _supabase.storage.from('campaign-images').upload(fname, file, { upsert: false }); 155 if (error) { toast('Error al subir: ' + error.message, 'error'); return; } 156 157 const { data: urlData } = _supabase.storage.from('campaign-images').getPublicUrl(fname); 158 uploadedImgUrl = urlData.publicUrl; 159 document.getElementById('campaignImageUrl').value = uploadedImgUrl; 160 toast('â Imagen subida'); 161} 162 163function previewImageUrl(val) { 164 if (val.startsWith('http')) setImgPreview(val); 165} 166 167function setImgPreview(src) { 168 document.getElementById('imgPreview').src = src; 169 document.getElementById('imgPreviewWrap').style.display = 'block'; 170 document.getElementById('uploadArea').style.display = 'none'; 171} 172 173function removeImage() { 174 document.getElementById('imgPreviewWrap').style.display = 'none'; 175 document.getElementById('uploadArea').style.display = 'flex'; 176 document.getElementById('imageInput').value = ''; 177 document.getElementById('campaignImageUrl').value = ''; 178 uploadedImgUrl = null; 179} 180 181async function saveCampaign() { 182 const name = document.getElementById('campaignName').value.trim(); 183 const title = document.getElementById('campaignTitle').value.trim(); 184 const subtitle = document.getElementById('campaignSubtitle').value.trim(); 185 const cta = document.getElementById('campaignCta').value.trim(); 186 const imgUrl = document.getElementById('campaignImageUrl').value.trim(); 187 const isActive = document.getElementById('campaignActive').checked; 188 189 if (!name || !title) { toast('Nombre y tÃtulo son obligatorios', 'error'); return; } 190 191 // Desactivar todas si esta se activa 192 if (isActive) { 193 await _supabase.from('campaigns').update({ is_active: false }).not('id', 'is', null); 194 } 195 196 const payload = { 197 name, hero_title: title, hero_subtitle: subtitle, 198 hero_cta_text: cta || 'Ver Carta', hero_image_url: imgUrl, is_active: isActive, 199 }; 200 201 const { error } = editCampaignId 202 ? await _supabase.from('campaigns').update(payload).eq('id', editCampaignId) 203 : await _supabase.from('campaigns').insert(payload); 204 205 if (error) { toast('Error: ' + error.message, 'error'); return; } 206 207 toast(editCampaignId ? 'â Campaña actualizada' : 'â Campaña creada'); 208 document.getElementById('campaignModal').classList.remove('open'); 209 await loadCampaigns(); 210} 211 212async function activateCampaign(id) { 213 await _supabase.from('campaigns').update({ is_active: false }).not('id', 'is', null); 214 const { error } = await _supabase.from('campaigns').update({ is_active: true }).eq('id', id); 215 if (error) { toast('Error al activar', 'error'); return; } 216 toast('â ¡Campaña activada! El sitio ya se actualizó.'); 217 await loadCampaigns(); 218} 219 220async function deleteCampaign(id) { 221 const c = campaigns.find(x => x.id === id); 222 if (c?.is_active) { toast('No puedes eliminar la campaña activa', 'error'); return; } 223 if (!confirm(`¿Eliminar la campaña "${c?.name}"?`)) return; 224 const { error } = await _supabase.from('campaigns').delete().eq('id', id); 225 if (error) { toast('Error al eliminar', 'error'); return; } 226 toast('ð Campaña eliminada'); 227 await loadCampaigns(); 228} 229 230// âââââââââââââââââââââââââââââââââââââââââââââââ 231// MENà 232// âââââââââââââââââââââââââââââââââââââââââââââââ 233async function loadMenuItems() { 234 const { data, error } = await _supabase 235 .from('menu_items').select('*').order('sort_order', { ascending: true }); 236 if (error) { toast('Error cargando menú', 'error'); return; } 237 menuItems = data || []; 238 renderMenuTable(menuItems); 239} 240 241function filterMenu(cat, btn) { 242 document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active')); 243 btn.classList.add('active'); 244 const items = cat === 'all' ? menuItems : menuItems.filter(i => i.category === cat); 245 renderMenuTable(items); 246} 247 248function renderMenuTable(items) { 249 const wrap = document.getElementById('menuTable'); 250 if (!items.length) { 251 wrap.innerHTML = '<div class="empty-state">No hay platos en esta categorÃa.</div>'; 252 return; 253 } 254 wrap.innerHTML = ` 255 <table> 256 <thead> 257 <tr> 258 <th></th><th>Plato</th><th>CategorÃa</th> 259 <th>Precio</th><th>Estado</th><th>Acciones</th> 260 </tr> 261 </thead> 262 <tbody> 263 ${items.map(item => ` 264 <tr class="${!item.available ? 'row-disabled' : ''}"> 265 <td class="td-emoji">${item.emoji || 'ð'}</td> 266 <td> 267 <div class="item-name">${item.name}</div> 268 <div class="item-desc">${item.description || ''}</div> 269 </td> 270 <td><span class="cat-badge">${item.category}</span></td> 271 <td class="td-price">S/. ${parseFloat(item.price).toFixed(2)}</td> 272 <td> 273 <button class="btn-toggle ${item.available ? 'on' : 'off'}" 274 onclick="toggleAvailable('${item.id}', ${item.available})"> 275 ${item.available ? 'â Disponible' : 'â No disponible'} 276 </button> 277 </td> 278 <td> 279 <div class="row-actions"> 280 <button class="icon-btn edit" onclick="openMenuModal('${item.id}')">âï¸</button> 281 <button class="icon-btn delete" onclick="deleteMenuItem('${item.id}')">ð</button> 282 </div> 283 </td> 284 </tr> 285 `).join('')} 286 </tbody> 287 </table> 288 `; 289} 290 291function openMenuModal(id = null) { 292 editMenuItemId = id; 293 ['menuItemId','menuName','menuDesc','menuPrice','menuImageUrl'].forEach(f => 294 document.getElementById(f).value = ''); 295 document.getElementById('menuCategory').value = 'Pollos a la Brasa'; 296 document.getElementById('menuEmoji').value = 'ð'; 297 document.getElementById('menuAvailable').checked = true; 298 299 if (id) { 300 const item = menuItems.find(x => x.id === id); 301 if (!item) return; 302 document.getElementById('menuModalTitle').textContent = 'Editar Plato'; 303 document.getElementById('menuItemId').value = item.id; 304 document.getElementById('menuName').value = item.name; 305 document.getElementById('menuCategory').value = item.category; 306 document.getElementById('menuDesc').value = item.description || ''; 307 document.getElementById('menuPrice').value = item.price; 308 document.getElementById('menuEmoji').value = item.emoji || 'ð'; 309 document.getElementById('menuImageUrl').value = item.image_url || ''; 310 document.getElementById('menuAvailable').checked = item.available; 311 } else { 312 document.getElementById('menuModalTitle').textContent = 'Nuevo Plato'; 313 } 314 document.getElementById('menuModal').classList.add('open'); 315} 316 317function closeMenuModal(e) { 318 if (e && e.target !== e.currentTarget) return; 319 document.getElementById('menuModal').classList.remove('open'); 320 editMenuItemId = null; 321} 322 323async function saveMenuItem() { 324 const name = document.getElementById('menuName').value.trim(); 325 const price = parseFloat(document.getElementById('menuPrice').value); 326 if (!name) { toast('El nombre es obligatorio', 'error'); return; } 327 if (isNaN(price)||price < 0) { toast('Precio inválido', 'error'); return; } 328 329 const payload = { 330 name, price, 331 category : document.getElementById('menuCategory').value, 332 description : document.getElementById('menuDesc').value.trim(), 333 emoji : document.getElementById('menuEmoji').value || 'ð', 334 image_url : document.getElementById('menuImageUrl').value.trim(), 335 available : document.getElementById('menuAvailable').checked, 336 }; 337 338 const { error } = editMenuItemId 339 ? await _supabase.from('menu_items').update(payload).eq('id', editMenuItemId) 340 : await _supabase.from('menu_items').insert({ ...payload, sort_order: menuItems.length + 1 }); 341 342 if (error) { toast('Error: ' + error.message, 'error'); return; } 343 toast(editMenuItemId ? 'â Plato actualizado' : 'â Plato agregado'); 344 document.getElementById('menuModal').classList.remove('open'); 345 await loadMenuItems(); 346} 347 348async function toggleAvailable(id, current) { 349 const { error } = await _supabase 350 .from('menu_items').update({ available: !current }).eq('id', id); 351 if (error) { toast('Error', 'error'); return; } 352 toast(!current ? 'â Plato habilitado' : '⸠Plato deshabilitado'); 353 await loadMenuItems(); 354} 355 356async function deleteMenuItem(id) { 357 const item = menuItems.find(x => x.id === id); 358 if (!confirm(`¿Eliminar "${item?.name}"?`)) return; 359 const { error } = await _supabase.from('menu_items').delete().eq('id', id); 360 if (error) { toast('Error', 'error'); return; } 361 toast('ð Plato eliminado'); 362 await loadMenuItems(); 363} 364 365// âââââââââââââââââââââââââââââââââââââââââââââââ 366// CONFIGURACIÃN 367// âââââââââââââââââââââââââââââââââââââââââââââââ 368async function loadConfigForm() { 369 const { data } = await _supabase.from('site_config').select('*'); 370 const cfg = {}; 371 if (data) data.forEach(r => cfg[r.key] = r.value); 372 373 // Mensajes por defecto 374 const defReserva = cfg.msg_reserva || 'ð *RESERVA â Fast Chicken AQP*\n\nð¤ N
374ombre: {nombre}\nð± Teléfono: {telefono}\nð Fecha: {fecha}\nð¥ Personas: {personas}\nð¬ Mensaje: {mensaje}'; 375 const defPedido = cfg.msg_pedido || '¡Hola! Quisiera hacer un pedido ð'; 376 const defItem = cfg.msg_item || '¡Hola! Quisiera pedir: *{plato}* (S/ {precio}) ð'; 377 const defTrabajo = cfg.msg_trabajo || 'Hola, me gustarÃa postular para trabajar en Fast Chicken AQP ð'; 378 379 document.getElementById('configForm').innerHTML = ` 380 <!-- LOGO --> 381 <div class="cfg-section"> 382 <h3>ð¼ï¸ Logo del restaurante</h3> 383 <div class="logo-upload-area" id="logoUploadArea"> 384 ${cfg.logo_url 385 ? `<img src="${cfg.logo_url}" style="max-height:80px;max-width:200px;object-fit:contain;border-radius:8px;" id="logoPreviewImg"> 386 <div style="margin-top:10px"> 387 <button class="btn-add" onclick="removeLogo()" style="background:#ef4444;padding:6px 14px;font-size:0.8rem">ð Quitar logo</button> 388 </div>` 389 : `<div style="text-align:center;padding:10px 0"> 390 <div style="font-size:2.5rem;margin-bottom:6px">ð¼ï¸</div> 391 <div style="color:var(--muted);font-size:0.85rem;margin-bottom:10px">Arrastra tu logo aquà o haz clic para subir</div> 392 <label for="logoFileInput" class="btn-add" style="cursor:pointer;padding:8px 18px;font-size:0.83rem">ð Seleccionar archivo</label> 393 <input type="file" id="logoFileInput" accept="image/png,image/svg+xml,image/webp" style="display:none" onchange="uploadLogo(this)"> 394 </div>` 395 } 396 </div> 397 <div class="logo-note"> 398 <span>ð</span> 399 <div> 400 <strong>Requisitos del logo:</strong><br> 401 ⢠Formato: <strong>PNG con fondo transparente</strong> (sin fondo blanco o de color)<br> 402 ⢠Tamaño recomendado: <strong>400 à 200 px</strong> como mÃnimo<br> 403 ⢠Peso máximo: <strong>500 KB</strong><br> 404 ⢠También se acepta SVG o WebP con transparencia<br> 405 ⢠El logo aparecerá en el navbar superior del sitio web 406 </div> 407 </div> 408 ${cfg.logo_url ? `<div class="field-group" style="margin-top:10px"><label>URL actual del logo</label><input type="text" id="cfgLogoUrl" value="${cfg.logo_url}" placeholder="https://..."><small style="color:var(--muted)">Puedes pegar una URL directamente</small></div>` : `<div class="field-group" style="margin-top:10px"><label>O pega una URL de imagen</label><input type="text" id="cfgLogoUrl" value="" placeholder="https://tudominio.com/logo.png" oninput="previewLogoUrl(this.value)"><small style="color:var(--muted)">La URL debe apuntar a una imagen PNG con fondo transparente</small></div>`} 409 </div> 410 411 <!-- CONTACTO --> 412 <div class="cfg-section"> 413 <h3>ð± Contacto</h3> 414 <div class="field-group"> 415 <label>WhatsApp (solo números, con código paÃs)</label> 416 <input type="text" id="cfgWhatsapp" value="${cfg.whatsapp||''}" placeholder="51987654321"> 417 </div> 418 <div class="field-group"> 419 <label>Email</label> 420 <input type="email" id="cfgEmail" value="${cfg.email||''}" placeholder="[email protected]"> 421 </div> 422 </div> 423 424 <!-- LOCALES --> 425 <div class="cfg-section"> 426 <h3>ð Locales</h3> 427 <div class="field-group"> 428 <label>Dirección Local 1</label> 429 <input type="text" id="cfgAddr1" value="${cfg.address_1||''}"> 430 </div> 431 <div class="field-group"> 432 <label>Dirección Local 2 (opcional)</label> 433 <input type="text" id="cfgAddr2" value="${cfg.address_2||''}"> 434 </div> 435 <div class="field-group"> 436 <label>Horario de atención</label> 437 <input type="text" id="cfgHours" value="${cfg.hours||''}" placeholder="Lun-Dom: 12:00 - 22:00"> 438 </div> 439 <div class="field-group"> 440 <label>ð Ubicación en Google Maps</label> 441 <input type="text" id="cfgMapsQuery" value="${cfg.maps_query||''}" placeholder="Fast Chicken AQP Arequipa"> 442 <small style="color:var(--muted)">Nombre exacto de tu local en Google Maps â el mapa del sitio se actualizará</small> 443 </div> 444 </div> 445 446 <!-- REDES SOCIALES --> 447 <div class="cfg-section"> 448 <h3>
448ð Redes Sociales</h3> 449 <div class="field-group"> 450 <label>URL de Facebook</label> 451 <input type="url" id="cfgFacebook" value="${cfg.facebook||''}"> 452 </div> 453 <div class="field-group"> 454 <label>URL de Instagram</label> 455 <input type="url" id="cfgInstagram" value="${cfg.instagram||''}"> 456 </div> 457 <div class="field-group"> 458 <label>URL de TikTok</label> 459 <input type="url" id="cfgTiktok" value="${cfg.tiktok||''}" placeholder="https://www.tiktok.com/@fastchickenaqp"> 460 </div> 461 </div> 462 463 <!-- MENSAJES DE WHATSAPP --> 464 <div class="cfg-section"> 465 <h3>ð¬ Mensajes de WhatsApp personalizados</h3> 466 <p style="color:var(--muted);font-size:0.83rem;margin-bottom:14px;line-height:1.6"> 467 Personaliza los mensajes que se envÃan automáticamente al WhatsApp del negocio.<br> 468 Puedes usar variables entre llaves que se reemplazan con los datos del cliente: 469 <strong>{nombre} {telefono} {fecha} {personas} {mensaje} {plato} {precio}</strong> 470 </p> 471 472 <div class="field-group"> 473 <label>ð Mensaje de Reserva</label> 474 <small style="color:var(--muted);display:block;margin-bottom:4px">Se usa cuando el cliente llena el formulario de reserva</small> 475 <textarea id="cfgMsgReserva" style="height:110px;font-family:monospace;font-size:0.82rem">${defReserva}</textarea> 476 </div> 477 478 <div class="field-group"> 479 <label>ð Mensaje general "Pedir Ahora"</label> 480 <small style="color:var(--muted);display:block;margin-bottom:4px">Se usa al hacer clic en el botón flotante de WhatsApp o "MI PEDIDO"</small> 481 <textarea id="cfgMsgPedido" style="height:60px;font-family:monospace;font-size:0.82rem">${defPedido}</textarea> 482 </div> 483 484 <div class="field-group"> 485 <label>ð Mensaje al agregar un plato al pedido</label> 486 <small style="color:var(--muted);display:block;margin-bottom:4px">Se usa al hacer clic en "Agregar" en un plato del menú. Variables: {plato} {precio}</small> 487 <textarea id="cfgMsgItem" style="height:60px;font-family:monospace;font-size:0.82rem">${defItem}</textarea> 488 </div> 489 490 <div class="field-group"> 491 <label>ð Mensaje "Trabaja con nosotros"</label> 492 <small style="color:var(--muted);display:block;margin-bottom:4px">Se usa en el acordeón de Nosotros al postular por WhatsApp</small> 493 <textarea id="cfgMsgTrabajo" style="height:60px;font-family:monospace;font-size:0.82rem">${defTrabajo}</textarea> 494 </div> 495 496 <div style="background:var(--card);border:1px solid var(--border);border-radius:8px;padding:12px;margin-top:6px"> 497 <div style="font-weight:800;font-size:0.83rem;margin-bottom:6px">ð¡ Ejemplo de variables disponibles:</div> 498 <div style="display:grid;grid-template-columns:1fr 1fr;gap:6px"> 499 ${[ 500 ['{nombre}','Nombre del cliente'], 501 ['{telefono}','Teléfono del cliente'], 502 ['{fecha}','Fecha de la reserva'], 503 ['{personas}','Cantidad de personas'], 504 ['{mensaje}','Mensaje adicional del cliente'], 505 ['{plato}','Nombre del plato seleccionado'], 506 ['{precio}','Precio del plato'], 507 ].map(([v,d]) => `<div style="font-size:0.78rem"><code style="background:var(--off);padding:2px 5px;border-radius:4px;color:var(--red)">${v}</code> <span style="color:var(--muted)">${d}</span></div>`).join('')} 508 </div> 509 </div> 510 </div> 511 512 <button class="btn-save-cfg" onclick="saveConfig()">ð¾ Guardar Configuración</button> 513 `; 514 515 // Hacer el área de logo droppable 516 setupLogoDropzone(); 517} 518 519async function uploadLogo(input) { 520 const file = input.files[0]; 521 if (!file) return; 522 toast('Subiendo logoâ¦', 'info'); 523 const fname = 'logo-' + Date.now() + '-' + file.name.replace(/[^a-z0-9.]/gi,'-').toLowerCase(); 524 const { error } = await _supabase.storage.from('campaign-images').upload(fname, file, { upsert: true }); 525 if (error) { toast('Error: ' + error.message, 'error'); return; } 526 const { data: urlData } = _supabase.storage.from('campaign-images').getPublicUrl(fname); 527 await _supabase.from('site_config').upsert({ key: 'logo_url', value: urlData.publicUrl }, { onConflict: 'key' }); 528 toast('â Logo subido correctamente'); 529 loadConfigForm(); 530} 531 532async function removeLogo() { 533 if (!confirm('¿Quitar el logo actual?')) return; 534 await _supabase.from('site_config').upsert({ key: 'logo_url', value: '' }, { onConflict: 'key' }); 535 toast('Logo eliminado'); 536 loadConfigForm(); 537} 538 539function previewLogoUrl(val) { 540 // optional preview 541} 542 543function setupLogoDropzone() { 544 const area = document.getElementById('logoUploadArea'); 545 if (!area) return; 546 area.addEventListener('dragover', e => { e.preventDefault(); area.style.borderColor = 'var(--red)'; }); 547 area.addEventListener('dragleave', () => { area.style.borderColor = 'var(--border)'; }); 548 area.addEventListener('drop', async e => { 549 e.preventDefault(); 550 area.style.borderColor = 'var(--border)'; 551 const file = e.dataTransfer.files[0]; 552 if (!file) return; 553 const fakeInput = { files: [file] }; 554 await uploadLogo(fakeInput); 555 }); 556} 557 558async function saveConfig() { 559 const rows = [ 560 { key:'whatsapp', value: document.getElementById('cfgWhatsapp')?.value || '' }, 561 { key:'email', value: document.getElementById('cfgEmail')?.value || '' }, 562 { key:'address_1', value: document.getElementById('cfgAddr1')?.value || '' }, 563 { key:'address_2', value: document.getElementById('cfgAddr2')?.value || '' }, 564 { key:'hours', value: document.getElementById('cfgHours')?.value || '' }, 565 { key:'facebook', value: document.getElementById('cfgFacebook')?.value || '' }, 566 { key:'instagram', value: document.getElementById('cfgInstagram')?.value || '' }, 567 { key:'tiktok', value: document.getElementById('cfgTiktok')?.value || '' }, 568 { key:'maps_query', value: document.getElementById('cfgMapsQuery')?.value || '' }, 569 { key:'logo_url', value: document.getElementById('cfgLogoUrl')?.value || '' }, 570 { key:'msg_reserva', value: document.getElementById('cfgMsgReserva')?.value || '' }, 571 { key:'msg_pedido', value: document.getElementById('cfgMsgPedido')?.value || '' }, 572 { key:'msg_item', value: document.getElementById('cfgMsgItem')?.value || '' }, 573 { key:'msg_trabajo', value: document.getElementById('cfgMsgTrabajo')?.value || '' }, 574 ]; 575 for (const row of rows) { 576 await _supabase.from('site_config').upsert(row, { onConflict: 'key' }); 577 } 578 toast('â Configuración guardada'); 579} 580 581// âââââââââââââââââââââââââââââââââââââââââââââââ 582// NOSOTROS 583// âââââââââââââââââââââââââââââââââââââââââââââââ 584 585// Claves que se editan como texto largo (textarea) 586const NOSOTROS_FIELDS = [
587 { key: 'nosotros_intro_1', label: 'ð Párrafo intro 1 (texto principal)', type: 'textarea', placeholder: 'Somos un restaurante de pollos a la brasa...' }, 588 { key: 'nosotros_intro_2', label: 'ð Párrafo intro 2 (texto secundario)', type: 'textarea', placeholder: 'Descubre más sobre quiénes somos...' }, 589 { key: 'nosotros_historia', label: 'ð Historia', type: 'textarea', placeholder: 'Cuéntanos cómo inició y evolucionó la marca...' }, 590 { key: 'nosotros_vision', label: 'ð¯ Visión', type: 'textarea', placeholder: 'Nuestra visión como empresa...' }, 591 { key: 'nosotros_mision', label: 'ð Misión', type: 'textarea', placeholder: 'Nuestra misión es...' }, 592 { key: 'nosotros_valores', label: 'ð Valores', type: 'textarea', placeholder: 'Uno por lÃnea. Ej:\nVerdad\nLealtad\nResponsabilidad' }, 593 { key: 'nosotros_certificaciones',label: 'ð Certificaciones', type: 'textarea', placeholder: 'Lista de certificaciones obtenidas...' }, 594 { key: 'nosotros_encuesta_url', label: 'ð Enlace Encuesta Satisfacción', type: 'input', placeholder: 'https://forms.google.com/...' }, 595 { key: 'nosotros_stat_anios', label: 'ð EstadÃstica: Años', type: 'input', placeholder: 'Ej: +5' }, 596 { key: 'nosotros_stat_locales', label: 'ð EstadÃstica: Locales', type: 'input', placeholder: 'Ej: 2' }, 597 { key: 'nosotros_stat_clientes', label: 'ð EstadÃstica: Clientes', type: 'input', placeholder: 'Ej: 10K+' }, 598]; 599 600async function loadNosotros() { 601 const { data } = await _supabase.from('site_config').select('*'); 602 const cfg = {}; 603 if (data) data.forEach(r => cfg[r.key] = r.value); 604 605 // Render fields 606 const form = document.getElementById('nosotrosForm'); 607 form.innerHTML = ` 608 <div class="cfg-section"> 609 <h3>ð Información General</h3> 610 ${NOSOTROS_FIELDS.map(f => ` 611 <div class="field-group"> 612 <label>${f.label}</label> 613 ${f.type === 'textarea' 614 ? `<textarea id="${f.key}" placeholder="${f.placeholder}" style="height:100px">${cfg[f.key] || ''}</textarea>` 615 : `<input type="text" id="${f.key}" value="${cfg[f.key] || ''}" placeholder="${f.placeholder}">` 616 } 617 </div> 618 `).join('')} 619 <button class="btn-save-cfg" onclick="saveNosotros()">ð¾ Guardar Información</button> 620 </div> 621 `; 622 623 // Render premios 624 renderPremios(cfg); 625} 626 627function renderPremios(cfg) { 628 let premios = []; 629 try { premios = JSON.parse(cfg.nosotros_premios || '[]'); } catch(_) {} 630 631 const wrap = document.getElementById('premiosSection'); 632 wrap.innerHTML = ` 633 <div class="cfg-section" style="margin-top:20px"> 634 <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px"> 635 <h3>ð Premios y Reconocimientos</h3> 636 <button class="btn-add" onclick="openPremioModal()" style="padding:8px 16px;font-size:0.82rem">+ Agregar Premio</button> 637 </div> 638 ${premios.length === 0 639 ? `<div class="empty-state" style="padding:32px">No hay premios registrados todavÃa.</div>` 640 : `<div style="display:flex;flex-direction:column;gap:10px"> 641 ${premios.map((p, i) => ` 642 <div style="display:flex;align-items:center;gap:14px;background:var(--card);border:1px solid var(--border);border-radius:10px;padding:14px 18px"> 643 <div style="font-size:1.8rem;flex-shrink:0">${p.emoji || 'ð'}</div> 644 <div style="flex:1"> 645 <div style="font-weight:900;color:var(--text);font-size:0.93rem">${p.titulo}</div> 646 <div style="color:var(--muted);font-size:0.8rem;margin-top:2px">${p.descripcion || ''}</div> 647 ${p.fecha ? `<div style="color:var(--gold);font-size:0.75rem;margin-top:3px">ð ${p.fecha}</div>` : ''} 648 </div> 649 <div style="display:flex;gap:6px"> 650 <button class="icon-btn edit" onclick="openPremioModal(${i})">âï¸</button> 651 <button class="icon-btn delete" onclick="deletePremio(${i})">ð</button> 652 </div> 653 </div> 654 `).join('')} 655 </div>` 656 } 657 </div> 658 `; 659} 660 661async function saveNosotros() { 662 const rows = NOSOTROS_FIELDS.map(f => ({ 663 key: f.key, 664 value: document.getElementById(f.key)?.value || '' 665 })); 666 667 for (const row of rows) { 668 await _supabase.from('site_config').upsert(row, { onConflict: 'key' }); 669 } 670 toast('â Información guardada'); 671} 672 673// ââ MODAL PREMIO ââââââââââââââââââââââââââââââ 674let editPremioIdx = null; 675 676function openPremioModal(idx = null) { 677 editPremioIdx = idx; 678 679 let titulo = '', descripcion = '', fecha = '', emoji = 'ð'; 680 if (idx !== null) { 681 // Load existing 682 _supabase.from('site_config').select('value').eq('key', 'nosotros_premios').single() 683 .then(({ data }) => { 684 try { 685 const premios = JSON.parse(data?.value || '[]'); 686 const p = premios[idx]; 687 if (p) { 688 document.getElementById('premioTitulo').value = p.titulo || ''; 689 document.getElementById('premioDesc').value = p.descripcion || ''; 690 document.getElementById('premioFecha').value = p.fecha || ''; 691 document.getElementById('premioEmoji').value = p.emoji || 'ð'; 692 } 693 } catch(_) {} 694 }); 695 } else { 696 document.getElementById('premioTitulo').value = ''; 697 document.getElementById('premioDesc').value = ''; 698 document.getElementById('premioFecha').value = ''; 699 document.getElementById('premioEmoji').value = 'ð'; 700 } 701 702 document.getElementById('premioModalTitle').textContent = idx !== null ? 'Editar Premio' : 'Nuevo Premio'; 703 document.getElementById('premioModal').classList.add('open'); 704} 705 706function closePremioModal(e) { 707 if (e && e.target !== e.currentTarget) return; 708 document.getElementById('premioModal').classList.remove('open'); 709} 710 711async function savePremio() { 712 const titulo = document.getElementById('premioTitulo').value.trim(); 713 if (!titulo) { toast('El tÃtulo es obligatorio', 'error'); return; } 714 715 const nuevo = { 716 titulo, 717 descripcion: document.getElementById('premioDesc').value.trim(), 718 fecha: document.getElementById('premioFecha').value.trim(), 719 emoji: document.getElementById('premioEmoji').value.trim() || 'ð', 720 }; 721 722 const { data } = await _supabase.from('site_config').select('value').eq('key', 'nosotros_premios').single(); 723 let premios = []; 724 try { premios = JSON.parse(data?.value || '[]'); } catch(_) {} 725 726 if (editPremioIdx !== null) { 727 premios[editPremioIdx] = nuevo; 728 } else { 729 premios.push(nuevo); 730 } 731 732 await _supabase.from('site_config').upsert({ key: 'nosotros_premios', value: JSON.stringify(premios) }, { onConflict: 'key' }); 733 734 toast(editPremioIdx !== null ? 'â Premio actualizado' : 'â Premio agregado'); 735 document.getElementById('premioModal').classList.remove('open'); 736 737 // Refresh premios section 738 const { data: fresh } = await _supabase.from('site_config').select('value').eq('key', 'nosotros_premios').single(); 739 const cfg = {}; 740 cfg.nosotros_premios = fresh?.value || '[]'; 741 renderPremios(cfg); 742} 743 744async function deletePremio(idx) { 745 if (!confirm('¿Eliminar este premio?')) return; 746 const { data } = await _supabase.from('site_config').select('value').eq('key', 'nosotros_premios').single(); 747 let premios = []; 748 try { premios = JSON.parse(data?.value || '[]'); } catch(_) {} 749 premios.splice(idx, 1); 750 await _supabase.from('site_config').upsert({ key: 'nosotros_premios', value: JSON.stringify(premios) }, { onConflict: 'key' }); 751 toast('ð Premio eliminado'); 752 const cfg = { nosotros_premios: JSON.stringify(premios) }; 753 renderPremios(cfg); 754} 755 756// âââ POPUP ADMIN ââââââââââââââââââââââââââââââââââ 757async function loadPopupTab() { 758 const { data } = await _supabase.from('site_config') 759 .select('value').eq('key','popup_config').maybeSingle(); 760 if (!data?.value) return; 761 try { 762 const cfg = typeof data.value === 'string' ? JSON.parse(data.value) : data.value; 763 document.getElementById('popupActive').checked = !!cfg.active; 764 document.getElementById('popupFrequency').value = cfg.frequency || 'session'; 765 document.getElementById('popupImageUrl').value = cfg.image_url || ''; 766 document.getElementById('popupDelayS').value = cfg.delay_ms ? cfg.delay_ms/1000 : 1; 767 document.getElementById('popupDuracionS').value = cfg.duracion_s || 0; 768 if (cfg.image_url) { 769 document.getElementById('popupImgPreview').src = cfg.image_url; 770 document.getElementById('popupImgPreviewWrap').style.display = 'block'; 771 } 772 } catch(_) {} 773} 774 775async function uploadPopupImage(input) { 776 const file = input.files[0]; 777 if (!file) return; 778 toast('Subiendo imagen...', 'info'); 779 const fname = `popup-${Date.now()}.${file.name.split('.').pop()}`; 780 const { error } = await _supabase.storage.from('campaign-images') 781 .upload(fname, file, { upsert: true }); 782 if (error) { toast('Error: ' + error.message, 'error'); return; } 783 const { data: u } = _supabase.storage.from('campaign-images').getPublicUrl(fname); 784 document.getElementById('popupImageUrl').value = u.publicUrl; 785 document.getElementById('popupImgPreview').src = u.publicUrl; 786 document.getElementById('popupImgPreviewWrap').style.display = 'block'; 787 toast('â Imagen subida'); 788} 789 790async function savePopup() { 791 const imgUrl = document.getElementById('popupImageUrl').value.trim(); 792 const active = document.getElementById('popupActive').checked; 793 if (active && !imgUrl) { toast('Sube o pega una URL de imagen primero', 'error'); return; } 794 const cfg = { 795 active, 796 frequency: document.getElementById('popupFrequency').value, 797 image_url: imgUrl, 798 delay_ms: Math.round((parseFloat(document.getElementById('popupDelayS').value)||1) * 1000), 799 duracion_s: parseInt(document.getElementById('popupDuracionS').value)||0, 800 }; 801 const { error } = await _supabase.from('site_config') 802 .upsert({ key:'popup_config', value: JSON.stringify(cfg) }, { onConflict:'key' }); 803 if (error) { toast('Error: '+error.message,'error'); return; } 804 toast(active ? 'â Pop-up activado y guardado' : 'â Pop-up desactivado'); 805} 806 807function testPopup() { 808 const url = document.getElementById('popupImageUrl').value.trim(); 809 if (!url) { toast('Primero sube o pega una URL de imagen','error'); return; } 810 const ex = document.getElementById('adminTestPopup'); 811 if (ex) ex.remove(); 812 const ov = document.createElement('div'); 813 ov.id = 'adminTestPopup'; 814 ov.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,.78);z-index:99999;display:flex;align-items:center;justify-content:center;padding:20px';
815 ov.onclick = (e)=>{ if(e.target===ov) ov.remove(); }; 816 ov.innerHTML = `<div style="max-width:440px;width:100%;position:relative"> 817 <button onclick="document.getElementById('adminTestPopup').remove()" 818 style="position:absolute;top:-14px;right:-14px;background:#fff;border:none;border-radius:50%;width:34px;height:34px;font-size:1rem;font-weight:900;cursor:pointer;box-shadow:0 4px 12px rgba(0,0,0,.3)">â</button> 819 <img src="${url}" style="width:100%;display:block;border-radius:14px;box-shadow:0 20px 60px rgba(0,0,0,.5)"> 820 </div>`; 821 document.body.appendChild(ov); 822}
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.