This commit is contained in:
Lucas Tettamanti
2026-02-04 16:06:51 -03:00
parent 2f8e267268
commit 5e79f17d00
21 changed files with 291 additions and 599 deletions

View File

@@ -1,13 +1,27 @@
import { api } from "../lib/api.js";
const DAYS = [
{ id: "lun", label: "Lunes", short: "Lun" },
{ id: "mar", label: "Martes", short: "Mar" },
{ id: "mie", label: "Miércoles", short: "Mié" },
{ id: "jue", label: "Jueves", short: "Jue" },
{ id: "vie", label: "Viernes", short: "Vie" },
{ id: "sab", label: "Sábado", short: "Sáb" },
{ id: "dom", label: "Domingo", short: "Dom" },
{ 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" },
];
// Lista oficial de 48 barrios de CABA
const CABA_BARRIOS = [
"Agronomía", "Almagro", "Balvanera", "Barracas", "Belgrano", "Boedo",
"Caballito", "Chacarita", "Coghlan", "Colegiales", "Constitución",
"Flores", "Floresta", "La Boca", "La Paternal", "Liniers",
"Mataderos", "Monte Castro", "Montserrat", "Nueva Pompeya", "Núñez",
"Palermo", "Parque Avellaneda", "Parque Chacabuco", "Parque Chas",
"Parque Patricios", "Puerto Madero", "Recoleta", "Retiro", "Saavedra",
"San Cristóbal", "San Nicolás", "San Telmo", "Vélez Sársfield", "Versalles",
"Villa Crespo", "Villa del Parque", "Villa Devoto", "Villa General Mitre",
"Villa Lugano", "Villa Luro", "Villa Ortúzar", "Villa Pueyrredón",
"Villa Real", "Villa Riachuelo", "Villa Santa Rita", "Villa Soldati", "Villa Urquiza"
];
class SettingsCrud extends HTMLElement {
@@ -113,6 +127,62 @@ class SettingsCrud extends HTMLElement {
}
.min-order-field { margin-top:16px; padding-top:16px; border-top:1px solid #1e2a3a; }
/* Zonas de entrega */
.zones-search { margin-bottom:12px; }
.zones-search input {
width:100%; padding:10px 14px;
background:#0f1520 url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%236c7a89'%3E%3Cpath d='M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z'/%3E%3C/svg%3E") no-repeat 12px center;
background-size:18px; padding-left:38px;
}
.zones-list { max-height:400px; overflow-y:auto; display:flex; flex-direction:column; gap:4px; }
.zone-row {
display:grid;
grid-template-columns:32px 1fr;
gap:12px;
align-items:start;
padding:10px 12px;
background:#0f1520;
border-radius:8px;
border:1px solid #1e2a3a;
transition:border-color .2s;
}
.zone-row.active { border-color:#1f6feb; background:#0f1825; }
.zone-row.hidden { display:none; }
.zone-toggle {
width:32px; height:18px; background:#253245; border-radius:9px;
cursor:pointer; position:relative; transition:background .2s; margin-top:2px;
}
.zone-toggle.active { background:#2ecc71; }
.zone-toggle::after {
content:''; position:absolute; top:2px; left:2px;
width:14px; height:14px; background:#fff; border-radius:50%;
transition:transform .2s;
}
.zone-toggle.active::after { transform:translateX(14px); }
.zone-content { display:flex; flex-direction:column; gap:8px; }
.zone-name { font-size:14px; color:#e7eef7; font-weight:500; }
.zone-config { display:none; gap:16px; flex-wrap:wrap; align-items:center; }
.zone-row.active .zone-config { display:flex; }
.zone-days { display:flex; gap:4px; }
.zone-day {
width:28px; height:28px; border-radius:6px;
background:#253245; color:#8aa0b5;
display:flex; align-items:center; justify-content:center;
font-size:11px; font-weight:600; cursor:pointer;
transition:all .15s;
}
.zone-day.active { background:#1f6feb; color:#fff; }
.zone-day:hover { background:#2d3e52; }
.zone-day.active:hover { background:#1a5fd0; }
.zone-cost { display:flex; align-items:center; gap:6px; }
.zone-cost label { font-size:12px; color:#8aa0b5; }
.zone-cost input { width:90px; padding:6px 10px; font-size:13px; text-align:right; }
.zones-summary {
margin-top:12px; padding:12px; background:#0f1520;
border-radius:8px; font-size:13px; color:#8aa0b5;
}
.zones-summary strong { color:#e7eef7; }
</style>
<div class="container">
@@ -138,6 +208,10 @@ class SettingsCrud extends HTMLElement {
if (!this.settings.schedule) {
this.settings.schedule = { delivery: {}, pickup: {} };
}
// Asegurar que delivery_zones existe
if (!this.settings.delivery_zones) {
this.settings.delivery_zones = {};
}
this.loading = false;
this.render();
} catch (e) {
@@ -201,6 +275,72 @@ class SettingsCrud extends HTMLElement {
}).join("");
}
// Convierte nombre de barrio a key (ej: "Villa Crespo" -> "villa_crespo")
barrioToKey(name) {
return name.toLowerCase()
.normalize("NFD").replace(/[\u0300-\u036f]/g, "") // quitar acentos
.replace(/\s+/g, "_");
}
getZoneConfig(barrioKey) {
return this.settings?.delivery_zones?.[barrioKey] || null;
}
setZoneConfig(barrioKey, config) {
if (!this.settings.delivery_zones) {
this.settings.delivery_zones = {};
}
if (config === null) {
delete this.settings.delivery_zones[barrioKey];
} else {
this.settings.delivery_zones[barrioKey] = config;
}
}
renderZonesList() {
return CABA_BARRIOS.map(barrio => {
const key = this.barrioToKey(barrio);
const config = this.getZoneConfig(key);
const isActive = config?.enabled === true;
const days = config?.days || [];
const cost = config?.delivery_cost || 0;
return `
<div class="zone-row ${isActive ? 'active' : ''}" data-barrio="${key}">
<div class="zone-toggle ${isActive ? 'active' : ''}" data-barrio="${key}"></div>
<div class="zone-content">
<span class="zone-name">${barrio}</span>
<div class="zone-config">
<div class="zone-days">
${DAYS.map(d => `
<div class="zone-day ${days.includes(d.id) ? 'active' : ''}"
data-barrio="${key}" data-day="${d.id}"
title="${d.label}">${d.short}</div>
`).join("")}
</div>
<div class="zone-cost">
<label>Costo:</label>
<input type="number" class="zone-cost-input" data-barrio="${key}"
value="${cost}" min="0" step="100" placeholder="0" />
</div>
</div>
</div>
</div>
`;
}).join("");
}
renderZonesSummary() {
const zones = this.settings?.delivery_zones || {};
const activeZones = Object.entries(zones).filter(([k, v]) => v?.enabled);
if (activeZones.length === 0) {
return `<div class="zones-summary">No hay zonas de entrega configuradas. Activá los barrios donde hacés delivery.</div>`;
}
return `<div class="zones-summary"><strong>${activeZones.length}</strong> zona${activeZones.length > 1 ? 's' : ''} de entrega activa${activeZones.length > 1 ? 's' : ''}</div>`;
}
render() {
const content = this.shadowRoot.getElementById("content");
@@ -290,6 +430,24 @@ class SettingsCrud extends HTMLElement {
</div>
</div>
<!-- Zonas de Entrega -->
<div class="panel">
<div class="panel-title">
<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 (Barrios CABA)
</div>
<div class="zones-search">
<input type="text" id="zoneSearch" placeholder="Buscar barrio..." />
</div>
<div class="zones-list" id="zonesList">
${this.renderZonesList()}
</div>
${this.renderZonesSummary()}
</div>
<div class="actions">
<button id="saveBtn" ${this.saving ? "disabled" : ""}>${this.saving ? "Guardando..." : "Guardar Configuración"}</button>
<button id="resetBtn" class="secondary">Restaurar</button>
@@ -359,6 +517,73 @@ class SettingsCrud extends HTMLElement {
// Reset button
this.shadowRoot.getElementById("resetBtn")?.addEventListener("click", () => this.load());
// Zone search
this.shadowRoot.getElementById("zoneSearch")?.addEventListener("input", (e) => {
const query = e.target.value.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "");
this.shadowRoot.querySelectorAll(".zone-row").forEach(row => {
const barrio = row.dataset.barrio;
const barrioName = CABA_BARRIOS.find(b => this.barrioToKey(b) === barrio) || "";
const normalized = barrioName.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "");
row.classList.toggle("hidden", query && !normalized.includes(query));
});
});
// Zone toggles
this.shadowRoot.querySelectorAll(".zone-toggle").forEach(toggle => {
toggle.addEventListener("click", () => {
const barrio = toggle.dataset.barrio;
const config = this.getZoneConfig(barrio);
if (config?.enabled) {
// Desactivar zona
this.setZoneConfig(barrio, null);
} else {
// Activar zona con días default (lun-sab)
this.setZoneConfig(barrio, {
enabled: true,
days: ["lun", "mar", "mie", "jue", "vie", "sab"],
delivery_cost: 0
});
}
this.render();
});
});
// Zone day toggles
this.shadowRoot.querySelectorAll(".zone-day").forEach(dayBtn => {
dayBtn.addEventListener("click", () => {
const barrio = dayBtn.dataset.barrio;
const day = dayBtn.dataset.day;
const config = this.getZoneConfig(barrio);
if (!config) return;
const days = config.days || [];
const idx = days.indexOf(day);
if (idx >= 0) {
days.splice(idx, 1);
} else {
days.push(day);
}
config.days = days;
this.setZoneConfig(barrio, config);
// Update UI without full re-render
dayBtn.classList.toggle("active", days.includes(day));
});
});
// Zone cost inputs
this.shadowRoot.querySelectorAll(".zone-cost-input").forEach(input => {
input.addEventListener("change", () => {
const barrio = input.dataset.barrio;
const config = this.getZoneConfig(barrio);
if (!config) return;
config.delivery_cost = parseFloat(input.value) || 0;
this.setZoneConfig(barrio, config);
});
});
}
collectScheduleFromInputs() {
@@ -386,6 +611,9 @@ class SettingsCrud extends HTMLElement {
// Collect schedule from inputs
const schedule = this.collectScheduleFromInputs();
// Collect delivery zones (already in settings from event handlers)
const delivery_zones = this.settings.delivery_zones || {};
const data = {
store_name: this.shadowRoot.getElementById("storeName")?.value || "",
bot_name: this.shadowRoot.getElementById("botName")?.value || "",
@@ -395,6 +623,7 @@ class SettingsCrud extends HTMLElement {
pickup_enabled: this.settings.pickup_enabled,
delivery_min_order: parseFloat(this.shadowRoot.getElementById("deliveryMinOrder")?.value) || 0,
schedule,
delivery_zones,
};
// Update settings with form values