Files
botino/public/components/settings-crud.js
Lucas Tettamanti cbbb88c052 Config: layout más amplio + toolbar sticky con Guardar
Antes: container 800px centrado, 3 panels stacked vertical, save al final.
Ahora:
- Container 1600px max, padding 24px.
- Toolbar sticky arriba con título "Configuración" + Restaurar + Guardar
  (reemplaza la sección final).
- Settings grid de 2 columnas (340px / 1fr) que colapsa a 1 col en <960px:
  - Izq: panel "Información del Negocio" (campos apilados, más densos) +
    panel "Retiro en Tienda" (toggle + grid).
  - Der: panel "Zonas de Entrega" full-width — el mapa ocupa la mayor parte
    de la pantalla (height:calc(100vh-220px), min 520px).
- zones-layout dentro del panel: 300px lista/form / 1fr mapa flex.
- Sin pérdida funcional: mismos campos, mismas validaciones, mismas tools.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 17:35:31 -03:00

769 lines
31 KiB
JavaScript

import { api } from "../lib/api.js";
const DAYS = [
{ id: "lun", label: "Lunes", short: "L" },
{ id: "mar", label: "Martes", short: "M" },
{ id: "mie", label: "Miércoles", short: "X" },
{ id: "jue", label: "Jueves", short: "J" },
{ id: "vie", label: "Viernes", short: "V" },
{ id: "sab", label: "Sábado", short: "S" },
{ id: "dom", label: "Domingo", short: "D" },
];
function makeZoneId(name) {
return String(name || "").toLowerCase()
.normalize("NFD").replace(/[̀-ͯ]/g, "")
.replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "")
.slice(0, 40) || `zona-${Date.now().toString(36)}`;
}
class SettingsCrud extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
this.settings = null;
this.loading = false;
this.saving = false;
this.zones = [];
this.selectedZoneId = null;
this._mapEditor = null;
this.shadowRoot.innerHTML = `
<style>
:host { display:block; height:100%; padding:16px 24px 24px; overflow:auto; }
* { box-sizing:border-box; font-family:var(--font-sans, system-ui); }
.container { max-width:1600px; margin:0 auto; }
.settings-grid {
display:grid;
grid-template-columns:minmax(320px, 360px) minmax(0, 1fr);
gap:16px;
align-items:start;
}
@media (max-width: 960px) {
.settings-grid { grid-template-columns:1fr; }
}
.col { display:flex; flex-direction:column; gap:16px; }
.panel { background:var(--panel); border:1px solid var(--border); border-radius:10px; padding:20px; }
.panel-zones { padding:16px 16px 20px; display:flex; flex-direction:column; min-height:560px; }
.toolbar {
display:flex; align-items:center; justify-content:space-between;
gap:16px; padding:12px 4px 16px;
position:sticky; top:0; z-index:5; background:var(--bg, #f7fafc);
border-bottom:1px solid var(--border);
margin-bottom:16px;
}
.toolbar-title { margin:0; font-size:18px; font-weight:600; color:var(--text); }
.toolbar-actions { display:flex; gap:8px; }
.toolbar-actions button { padding:8px 16px; font-size:13px; }
.zones-map-wrap { display:flex; min-height:520px; min-width:0; }
.zones-map-wrap zone-map-editor { flex:1; height:auto; min-height:520px; }
.panel-title { font-size:16px; font-weight:700; color:var(--text); margin-bottom:16px; display:flex; align-items:center; gap:8px; }
.panel-title svg { width:20px; height:20px; fill:var(--accent); }
.form-row { display:grid; grid-template-columns:1fr 1fr; gap:16px; margin-bottom:16px; }
.form-row.full { grid-template-columns:1fr; }
.field { }
.field label { display:block; font-size:12px; color:var(--text-muted); margin-bottom:6px; text-transform:uppercase; letter-spacing:.4px; }
.field-hint { font-size:11px; color:var(--text-muted); margin-top:4px; }
input, select, textarea {
background:var(--panel-2); color:var(--text); border:1px solid var(--border-hi);
border-radius:8px; padding:10px 14px; font-size:14px; width:100%;
}
input:focus, select:focus, textarea:focus { outline:none; border-color:var(--accent); }
input:disabled { opacity:.6; cursor:not-allowed; }
button {
cursor:pointer; background:var(--accent); color:#fff; border:none;
border-radius:8px; padding:10px 20px; font-size:14px; font-weight:600;
}
button:hover { background:var(--accent-hover); }
button:disabled { opacity:.5; cursor:not-allowed; }
button.secondary { background:var(--border-hi); }
button.secondary:hover { background:var(--border-hi); }
.toggle-row { display:flex; align-items:center; gap:12px; margin-bottom:16px; }
.toggle {
position:relative; width:48px; height:26px;
background:var(--border-hi); border-radius:13px; cursor:pointer;
transition:background .2s; flex-shrink:0;
}
.toggle.active { background:var(--accent); }
.toggle::after {
content:''; position:absolute; top:3px; left:3px;
width:20px; height:20px; background:#fff; border-radius:50%;
transition:transform .2s;
}
.toggle.active::after { transform:translateX(22px); }
.toggle-label { font-size:14px; color:var(--text); }
/* Schedule grid */
.schedule-grid { display:flex; flex-direction:column; gap:8px; }
.schedule-row {
display:grid;
grid-template-columns:90px 32px 1fr;
gap:12px;
align-items:center;
padding:8px 12px;
background:var(--panel-2);
border-radius:8px;
border:1px solid var(--border);
}
.schedule-row.disabled { opacity:0.4; }
.day-label { font-size:13px; color:var(--text); font-weight:500; }
.day-toggle {
width:32px; height:18px; background:var(--border-hi); border-radius:9px;
cursor:pointer; position:relative; transition:background .2s;
}
.day-toggle.active { background:var(--ok); }
.day-toggle::after {
content:''; position:absolute; top:2px; left:2px;
width:14px; height:14px; background:#fff; border-radius:50%;
transition:transform .2s;
}
.day-toggle.active::after { transform:translateX(14px); }
.hours-inputs { display:flex; align-items:center; gap:8px; }
.hours-inputs input {
width:70px; text-align:center; font-family:monospace;
font-size:13px; padding:6px 8px; letter-spacing:1px;
}
.hours-inputs span { color:var(--text-muted); font-size:12px; }
.hours-inputs.disabled input { opacity:0.4; pointer-events:none; }
.actions { display:flex; gap:12px; margin-top:24px; }
.loading { text-align:center; padding:60px; color:var(--text-muted); }
.success-msg {
background:var(--ok)30; border:1px solid var(--ok);
color:var(--ok); padding:12px 16px; border-radius:8px;
margin-bottom:16px; font-size:14px;
}
.error-msg {
background:var(--err)30; border:1px solid var(--err);
color:var(--err); padding:12px 16px; border-radius:8px;
margin-bottom:16px; font-size:14px;
}
.min-order-field { margin-top:16px; padding-top:16px; border-top:1px solid var(--border); }
/* Zonas de entrega — editor con mapa */
.zones-layout { display:grid; grid-template-columns:300px minmax(0,1fr); gap:16px; height:calc(100vh - 220px); min-height:520px; }
@media (max-width: 1100px) { .zones-layout { grid-template-columns:1fr; height:auto; } }
.zones-side { display:flex; flex-direction:column; gap:8px; min-width:0; min-height:0; }
.zones-side-header { display:flex; align-items:center; justify-content:space-between; }
.zones-side-header h4 { margin:0; font-size:13px; color:var(--text); }
.zones-side-header button { padding:6px 10px; font-size:12px; }
.zones-list { display:flex; flex-direction:column; gap:6px; flex:1; min-height:120px; max-height:50%; overflow-y:auto; padding-right:4px; }
.zone-row {
display:flex; align-items:center; gap:10px;
padding:10px 12px; border-radius:var(--r-md, 10px);
background:var(--panel-2); border:1px solid var(--border);
cursor:pointer; transition:border-color .15s, background .15s;
}
.zone-row:hover { border-color:var(--border-hi); }
.zone-row.active { border-color:var(--accent); background:var(--accent-soft); }
.zone-row.disabled { opacity:.55; }
.zone-swatch { width:14px; height:14px; border-radius:4px; flex-shrink:0; }
.zone-row-main { flex:1; min-width:0; display:flex; flex-direction:column; gap:2px; }
.zone-row-name { font-size:13px; font-weight:500; color:var(--text); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.zone-row-meta { font-size:11px; color:var(--text-muted); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.zones-empty { padding:24px; text-align:center; color:var(--text-muted); font-size:13px; }
.zone-form { padding:14px; background:var(--panel-2); border:1px solid var(--border); border-radius:var(--r-md, 10px); display:flex; flex-direction:column; gap:12px; }
.zone-form .row { display:grid; grid-template-columns:1fr 1fr; gap:12px; }
.zone-form .row.three { grid-template-columns:2fr 1fr 1fr; }
.zone-form label { font-size:11px; color:var(--text-muted); text-transform:uppercase; letter-spacing:.4px; }
.zone-form input { padding:8px 10px; font-size:13px; }
.zone-days-pick { display:flex; gap:4px; flex-wrap:wrap; }
.zone-day-pick { padding:4px 8px; border-radius:6px; font-size:11px; font-weight:600; cursor:pointer;
background:var(--border); color:var(--text-muted); border:1px solid transparent; }
.zone-day-pick.active { background:var(--accent); color:var(--text-on-acc, #fff); }
.zone-row-actions { display:flex; gap:8px; justify-content:flex-end; }
.zone-row-actions button { padding:6px 10px; font-size:12px; }
.zone-row-actions .danger { background:var(--err); }
.zones-summary { margin-top:8px; padding:10px 12px; background:var(--panel-2); border-radius:var(--r-md, 10px); font-size:12px; color:var(--text-muted); }
.zones-summary strong { color:var(--text); }
</style>
<div class="container">
<div id="messages"></div>
<div id="content">
<div class="loading">Cargando configuración...</div>
</div>
</div>
`;
}
connectedCallback() {
this.load();
}
async load() {
this.loading = true;
this.render();
try {
this.settings = await api.getSettings();
if (!this.settings.schedule) {
this.settings.schedule = { delivery: {}, pickup: {} };
}
const dz = this.settings.delivery_zones || {};
this.zones = Array.isArray(dz.zones) ? dz.zones.map((z) => ({ ...z })) : [];
this.selectedZoneId = this.zones[0]?.id || null;
this.loading = false;
this.render();
} catch (e) {
console.error("Error loading settings:", e);
this.loading = false;
this.showError("Error cargando configuración: " + e.message);
}
}
getScheduleSlot(type, dayId) {
return this.settings?.schedule?.[type]?.[dayId] || null;
}
setScheduleSlot(type, dayId, slot) {
if (!this.settings.schedule) {
this.settings.schedule = { delivery: {}, pickup: {} };
}
if (!this.settings.schedule[type]) {
this.settings.schedule[type] = {};
}
this.settings.schedule[type][dayId] = slot;
}
renderScheduleGrid(type, enabled) {
const defaultStart = type === "delivery" ? "09:00" : "08:00";
const defaultEnd = type === "delivery" ? "18:00" : "20:00";
return DAYS.map(day => {
const slot = this.getScheduleSlot(type, day.id);
const isActive = slot !== null && slot !== undefined;
const start = slot?.start || defaultStart;
const end = slot?.end || defaultEnd;
return `
<div class="schedule-row ${!enabled ? 'disabled' : ''}">
<span class="day-label">${day.label}</span>
<div class="day-toggle ${isActive ? 'active' : ''}"
data-type="${type}" data-day="${day.id}"
${!enabled ? 'style="pointer-events:none"' : ''}></div>
<div class="hours-inputs ${!isActive ? 'disabled' : ''}">
<input type="text"
class="hour-start"
data-type="${type}"
data-day="${day.id}"
value="${start}"
placeholder="09:00"
maxlength="5"
${!enabled || !isActive ? 'disabled' : ''} />
<span>a</span>
<input type="text"
class="hour-end"
data-type="${type}"
data-day="${day.id}"
value="${end}"
placeholder="18:00"
maxlength="5"
${!enabled || !isActive ? 'disabled' : ''} />
</div>
</div>
`;
}).join("");
}
ZONE_PALETTE_VARS() {
return ["--chart-blue", "--chart-green", "--chart-purple", "--chart-orange", "--chart-pink", "--chart-gray"];
}
zoneSwatchColor(idx) {
const palette = this.ZONE_PALETTE_VARS();
return `var(${palette[idx % palette.length]})`;
}
formatDaysShort(days) {
if (!Array.isArray(days) || !days.length) return "\u2014";
const order = ["lun","mar","mie","jue","vie","sab","dom"];
const idx = days.map((d) => order.indexOf(d)).filter((i) => i >= 0).sort((a, b) => a - b);
if (idx.length >= 3 && idx.every((v, i, arr) => i === 0 || v === arr[i - 1] + 1)) {
return `${order[idx[0]]}-${order[idx[idx.length - 1]]}`;
}
return idx.map((i) => order[i]).join("/");
}
renderZonesList() {
if (!this.zones.length) {
return `<div class="zones-empty">No hay zonas dibujadas todavía. Tocá <strong>Crear zona</strong> y dibujá un polígono en el mapa.</div>`;
}
return this.zones.map((z, i) => {
const cost = z.delivery_cost != null ? `$${Number(z.delivery_cost).toLocaleString("es-AR")}` : "";
const days = this.formatDaysShort(z.delivery_days);
const start = z.delivery_hours?.start?.slice(0, 5) || "";
const end = z.delivery_hours?.end?.slice(0, 5) || "";
const hours = start && end ? `${start}-${end}` : "";
const meta = [cost, days, hours].filter(Boolean).join(" · ");
const active = z.id === this.selectedZoneId ? "active" : "";
const disabled = z.enabled === false ? "disabled" : "";
return `
<div class="zone-row ${active} ${disabled}" data-zone-id="${z.id}">
<div class="zone-swatch" style="background:${this.zoneSwatchColor(i)}"></div>
<div class="zone-row-main">
<div class="zone-row-name">${this.escapeHtml(z.name || z.id)}</div>
<div class="zone-row-meta">${this.escapeHtml(meta || "sin configurar")}</div>
</div>
</div>
`;
}).join("");
}
renderZoneForm() {
const z = this.zones.find((x) => x.id === this.selectedZoneId);
if (!z) {
return `<div class="zones-empty">Seleccioná una zona en la lista o dibujá una nueva en el mapa para configurarla.</div>`;
}
const days = z.delivery_days || [];
const start = z.delivery_hours?.start?.slice(0, 5) || "10:00";
const end = z.delivery_hours?.end?.slice(0, 5) || "20:00";
return `
<div class="zone-form" data-zone-id="${z.id}">
<div class="row three">
<div class="field">
<label>Nombre</label>
<input type="text" id="zoneName" value="${this.escapeHtml(z.name || "")}" maxlength="60" />
</div>
<div class="field">
<label>Costo de envío ($)</label>
<input type="number" id="zoneCost" value="${z.delivery_cost ?? 0}" min="0" step="100" />
</div>
<div class="field">
<label>Mín. pedido ($)</label>
<input type="number" id="zoneMin" value="${z.min_order_amount ?? 0}" min="0" step="100" />
</div>
</div>
<div class="field">
<label>Días de entrega</label>
<div class="zone-days-pick">
${DAYS.map((d) => `
<span class="zone-day-pick ${days.includes(d.id) ? "active" : ""}" data-day="${d.id}" title="${d.label}">${d.short}</span>
`).join("")}
</div>
</div>
<div class="row">
<div class="field">
<label>Hora de inicio</label>
<input type="time" id="zoneStart" value="${start}" />
</div>
<div class="field">
<label>Hora de fin</label>
<input type="time" id="zoneEnd" value="${end}" />
</div>
</div>
<div class="row">
<div class="field">
<label>Estado</label>
<select id="zoneEnabled">
<option value="true" ${z.enabled !== false ? "selected" : ""}>Habilitada</option>
<option value="false" ${z.enabled === false ? "selected" : ""}>Deshabilitada (no recibe pedidos)</option>
</select>
</div>
<div class="zone-row-actions" style="align-self:end;">
<button class="secondary" id="zoneFitBtn">Centrar en mapa</button>
<button class="danger" id="zoneDeleteBtn">Eliminar zona</button>
</div>
</div>
</div>
`;
}
renderZonesSummary() {
const enabled = this.zones.filter((z) => z.enabled !== false);
if (!this.zones.length) {
return `<div class="zones-summary">Dibujá al menos una zona en el mapa para que el bot pueda confirmar envíos. Sin zonas, todas las direcciones quedan sin validar.</div>`;
}
return `<div class="zones-summary"><strong>${enabled.length}</strong> de <strong>${this.zones.length}</strong> zona${this.zones.length > 1 ? "s" : ""} habilitada${enabled.length === 1 ? "" : "s"}.</div>`;
}
render() {
const content = this.shadowRoot.getElementById("content");
if (this.loading) {
content.innerHTML = `<div class="loading">Cargando configuración...</div>`;
return;
}
if (!this.settings) {
content.innerHTML = `<div class="loading">No se pudo cargar la configuración</div>`;
return;
}
const s = this.settings;
content.innerHTML = `
<div class="toolbar">
<h2 class="toolbar-title">Configuración</h2>
<div class="toolbar-actions">
<button id="resetBtn" class="secondary" type="button">Restaurar</button>
<button id="saveBtn" type="button" ${this.saving ? "disabled" : ""}>
${this.saving ? "Guardando..." : "Guardar"}
</button>
</div>
</div>
<div class="settings-grid">
<div class="col">
<div class="panel">
<div class="panel-title">
<svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg>
Información del Negocio
</div>
<div class="field" style="margin-bottom:12px;">
<label>Nombre del negocio</label>
<input type="text" id="storeName" value="${this.escapeHtml(s.store_name || "")}" placeholder="Ej: Carnicería Don Pedro" />
<div class="field-hint">Se usa en los mensajes del bot</div>
</div>
<div class="field" style="margin-bottom:12px;">
<label>Nombre del bot</label>
<input type="text" id="botName" value="${this.escapeHtml(s.bot_name || "")}" placeholder="Ej: Piaf" />
<div class="field-hint">El asistente virtual</div>
</div>
<div class="field" style="margin-bottom:12px;">
<label>Dirección</label>
<input type="text" id="storeAddress" value="${this.escapeHtml(s.store_address || "")}" placeholder="Av. Corrientes 1234, CABA" />
</div>
<div class="field">
<label>Teléfono</label>
<input type="text" id="storePhone" value="${this.escapeHtml(s.store_phone || "")}" placeholder="+54 11 1234-5678" />
</div>
</div>
<div class="panel">
<div class="panel-title">
<svg viewBox="0 0 24 24"><path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-7 14l-5-5 1.41-1.41L12 14.17l4.59-4.58L18 11l-6 6z"/></svg>
Retiro en Tienda
</div>
<div class="toggle-row">
<div class="toggle ${s.pickup_enabled ? "active" : ""}" id="pickupToggle"></div>
<span class="toggle-label">Retiro habilitado</span>
</div>
<div class="schedule-grid" id="pickupSchedule">
${this.renderScheduleGrid("pickup", s.pickup_enabled)}
</div>
</div>
</div>
<div class="col">
<div class="panel panel-zones">
<div class="panel-title" style="margin-bottom:6px;">
<svg viewBox="0 0 24 24"><path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z"/></svg>
Zonas de Entrega
</div>
<div class="field-hint" style="margin-bottom:12px;">
Dibujá los polígonos en el mapa. Cada zona tiene su costo, días y rango horario.
El bot matchea con la ubicación que el cliente comparta por WhatsApp.
</div>
<div class="zones-layout">
<div class="zones-side">
<div class="zones-side-header">
<h4>Zonas</h4>
<button id="zoneCreateBtn" type="button">+ Crear zona</button>
</div>
<div class="zones-list" id="zonesList">
${this.renderZonesList()}
</div>
<div id="zoneFormSlot">
${this.renderZoneForm()}
</div>
${this.renderZonesSummary()}
</div>
<div class="zones-map-wrap">
<zone-map-editor id="zoneMapEditor"></zone-map-editor>
</div>
</div>
</div>
</div>
</div>
`;
this.setupEventListeners();
}
setupEventListeners() {
// Toggle pickup
const pickupToggle = this.shadowRoot.getElementById("pickupToggle");
pickupToggle?.addEventListener("click", () => {
this.settings.pickup_enabled = !this.settings.pickup_enabled;
this.render();
});
// Day toggles
this.shadowRoot.querySelectorAll(".day-toggle").forEach(toggle => {
toggle.addEventListener("click", () => {
const type = toggle.dataset.type;
const day = toggle.dataset.day;
const currentSlot = this.getScheduleSlot(type, day);
if (currentSlot) {
// Desactivar día
this.setScheduleSlot(type, day, null);
} else {
// Activar día con horarios default
const defaultStart = type === "delivery" ? "09:00" : "08:00";
const defaultEnd = type === "delivery" ? "18:00" : "20:00";
this.setScheduleSlot(type, day, { start: defaultStart, end: defaultEnd });
}
this.render();
});
});
// Hour inputs - update on blur
this.shadowRoot.querySelectorAll(".hour-start, .hour-end").forEach(input => {
input.addEventListener("blur", () => {
const type = input.dataset.type;
const day = input.dataset.day;
const isStart = input.classList.contains("hour-start");
const slot = this.getScheduleSlot(type, day);
if (!slot) return;
const value = input.value.trim();
if (isStart) {
slot.start = value || (type === "delivery" ? "09:00" : "08:00");
} else {
slot.end = value || (type === "delivery" ? "18:00" : "20:00");
}
this.setScheduleSlot(type, day, slot);
});
});
// Save button
this.shadowRoot.getElementById("saveBtn")?.addEventListener("click", () => this.save());
// Reset button
this.shadowRoot.getElementById("resetBtn")?.addEventListener("click", () => this.load());
this.setupZoneEditor();
}
setupZoneEditor() {
const editor = this.shadowRoot.getElementById("zoneMapEditor");
if (!editor) return;
this._mapEditor = editor;
editor.zones = this.zones;
if (this.selectedZoneId) editor.selectedId = this.selectedZoneId;
editor.addEventListener("change", (e) => {
const next = e.detail.zones || [];
const merged = [];
for (const z of next) {
const existing = this.zones.find((x) => x.id === z.id);
if (existing) merged.push({ ...existing, polygon: z.polygon });
else merged.push(z);
}
this.zones = merged;
this.refreshZonesPanel();
});
editor.addEventListener("select", (e) => {
this.selectedZoneId = e.detail.id || null;
this.refreshZonesPanel();
});
this.shadowRoot.getElementById("zoneCreateBtn")?.addEventListener("click", () => {
this._mapEditor?.startDrawing();
});
this.attachZoneSideListeners();
}
attachZoneSideListeners() {
this.shadowRoot.querySelectorAll(".zone-row[data-zone-id]").forEach((row) => {
row.addEventListener("click", () => {
const id = row.dataset.zoneId;
this.selectedZoneId = id;
if (this._mapEditor) this._mapEditor.selectedId = id;
this.refreshZonesPanel();
});
});
const z = this.zones.find((x) => x.id === this.selectedZoneId);
if (!z) return;
const onChange = () => this.refreshZonesList();
const nameEl = this.shadowRoot.getElementById("zoneName");
nameEl?.addEventListener("input", () => { z.name = nameEl.value; onChange(); });
const costEl = this.shadowRoot.getElementById("zoneCost");
costEl?.addEventListener("change", () => { z.delivery_cost = Number(costEl.value) || 0; onChange(); });
const minEl = this.shadowRoot.getElementById("zoneMin");
minEl?.addEventListener("change", () => { z.min_order_amount = Number(minEl.value) || 0; });
const startEl = this.shadowRoot.getElementById("zoneStart");
startEl?.addEventListener("change", () => {
z.delivery_hours = { ...(z.delivery_hours || {}), start: startEl.value };
onChange();
});
const endEl = this.shadowRoot.getElementById("zoneEnd");
endEl?.addEventListener("change", () => {
z.delivery_hours = { ...(z.delivery_hours || {}), end: endEl.value };
onChange();
});
const enabledEl = this.shadowRoot.getElementById("zoneEnabled");
enabledEl?.addEventListener("change", () => {
z.enabled = enabledEl.value === "true";
onChange();
});
this.shadowRoot.querySelectorAll(".zone-day-pick").forEach((btn) => {
btn.addEventListener("click", () => {
const day = btn.dataset.day;
const days = z.delivery_days || [];
const idx = days.indexOf(day);
if (idx >= 0) days.splice(idx, 1);
else days.push(day);
z.delivery_days = days;
btn.classList.toggle("active", days.includes(day));
onChange();
});
});
this.shadowRoot.getElementById("zoneFitBtn")?.addEventListener("click", () => {
if (this._mapEditor) this._mapEditor.selectedId = z.id;
});
this.shadowRoot.getElementById("zoneDeleteBtn")?.addEventListener("click", () => {
if (this._mapEditor) this._mapEditor.removeZone(z.id);
this.zones = this.zones.filter((x) => x.id !== z.id);
if (this.selectedZoneId === z.id) this.selectedZoneId = this.zones[0]?.id || null;
this.refreshZonesPanel();
});
}
refreshZonesPanel() {
const list = this.shadowRoot.getElementById("zonesList");
const formSlot = this.shadowRoot.getElementById("zoneFormSlot");
if (list) list.innerHTML = this.renderZonesList();
if (formSlot) formSlot.innerHTML = this.renderZoneForm();
this.attachZoneSideListeners();
}
refreshZonesList() {
const list = this.shadowRoot.getElementById("zonesList");
if (list) list.innerHTML = this.renderZonesList();
this.shadowRoot.querySelectorAll(".zone-row[data-zone-id]").forEach((row) => {
row.addEventListener("click", () => {
const id = row.dataset.zoneId;
this.selectedZoneId = id;
if (this._mapEditor) this._mapEditor.selectedId = id;
this.refreshZonesPanel();
});
});
}
collectScheduleFromInputs() {
// Sólo pickup: el horario de delivery vive ahora dentro de cada zona.
const schedule = { pickup: {} };
this.shadowRoot.querySelectorAll(`.hour-start[data-type="pickup"]`).forEach((input) => {
const day = input.dataset.day;
const endInput = this.shadowRoot.querySelector(`.hour-end[data-type="pickup"][data-day="${day}"]`);
const toggle = this.shadowRoot.querySelector(`.day-toggle[data-type="pickup"][data-day="${day}"]`);
if (toggle?.classList.contains("active")) {
schedule.pickup[day] = {
start: input.value.trim() || "08:00",
end: endInput?.value.trim() || "20:00",
};
}
});
return schedule;
}
async save() {
// Collect schedule from inputs
const schedule = this.collectScheduleFromInputs();
// Antes de serializar, refrescar polígonos desde el editor por si el
// usuario editó vértices y no llegó a disparar otro evento change.
if (this._mapEditor) {
const live = this._mapEditor.zones;
this.zones = this.zones.map((z) => {
const fromMap = live.find((x) => x.id === z.id);
return fromMap ? { ...z, polygon: fromMap.polygon } : z;
});
}
const cleanZones = this.zones
.filter((z) => z.polygon && Array.isArray(z.polygon.coordinates))
.map((z) => ({
id: z.id,
name: z.name || z.id,
polygon: z.polygon,
delivery_cost: Number(z.delivery_cost) || 0,
delivery_days: Array.isArray(z.delivery_days) ? z.delivery_days : [],
delivery_hours: z.delivery_hours || { start: "10:00", end: "20:00" },
min_order_amount: Number(z.min_order_amount) || 0,
enabled: z.enabled !== false,
}));
const delivery_zones = { zones: cleanZones };
const data = {
store_name: this.shadowRoot.getElementById("storeName")?.value || "",
bot_name: this.shadowRoot.getElementById("botName")?.value || "",
store_address: this.shadowRoot.getElementById("storeAddress")?.value || "",
store_phone: this.shadowRoot.getElementById("storePhone")?.value || "",
pickup_enabled: this.settings.pickup_enabled,
schedule,
delivery_zones,
};
// Update settings with form values
this.settings = { ...this.settings, ...data };
this.saving = true;
this.render();
try {
console.log("[settings-crud] Saving:", data);
const result = await api.saveSettings(data);
console.log("[settings-crud] Save result:", result);
if (result.ok === false) {
throw new Error(result.message || result.error || "Error desconocido");
}
this.settings = result.settings || data;
this.saving = false;
this.showSuccess(result.message || "Configuración guardada correctamente");
this.render();
} catch (e) {
console.error("[settings-crud] Error saving settings:", e);
this.saving = false;
this.showError("Error guardando: " + (e.message || e));
this.render();
}
}
escapeHtml(str) {
return (str || "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
}
showSuccess(msg) {
const messages = this.shadowRoot.getElementById("messages");
messages.innerHTML = `<div class="success-msg">${msg}</div>`;
setTimeout(() => { messages.innerHTML = ""; }, 4000);
}
showError(msg) {
const messages = this.shadowRoot.getElementById("messages");
messages.innerHTML = `<div class="error-msg">${msg}</div>`;
setTimeout(() => { messages.innerHTML = ""; }, 5000);
}
}
customElements.define("settings-crud", SettingsCrud);