PageSourceSearch

https://effortless-axolotl-51aa38.netlify.app/js/admin.js

js effortless-axolotl-51aa38.netlify.app collected 2026-10-03 10:41:07 UTC 38,286 bytes, 822 lines download raw bytes

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.