diff --git a/db/migrations/20260502181148_drop_legacy_zones_format.sql b/db/migrations/20260502181148_drop_legacy_zones_format.sql new file mode 100644 index 0000000..5439bd4 --- /dev/null +++ b/db/migrations/20260502181148_drop_legacy_zones_format.sql @@ -0,0 +1,13 @@ +-- migrate:up +-- Limpiar formato legacy de delivery_zones (caba.barrios, flat) y dejar solo +-- el schema nuevo { zones: [...] }. Pre-prod: no preservamos data legacy. +UPDATE tenant_settings +SET delivery_zones = '{}'::jsonb +WHERE + delivery_zones IS NULL + OR NOT (delivery_zones ? 'zones') + OR jsonb_typeof(delivery_zones->'zones') <> 'array'; + +-- migrate:down +-- noop: no preservamos el formato legacy. +SELECT 1; diff --git a/public/app.js b/public/app.js index 0190944..77a3bc5 100644 --- a/public/app.js +++ b/public/app.js @@ -12,6 +12,7 @@ import "./components/quantities-crud.js"; import "./components/orders-crud.js"; import "./components/test-panel.js"; import "./components/takeovers-crud.js"; +import "./components/zone-map-editor.js"; import "./components/settings-crud.js"; import { connectSSE } from "./lib/sse.js"; import { initRouter } from "./lib/router.js"; diff --git a/public/components/settings-crud.js b/public/components/settings-crud.js index b355632..39c542e 100644 --- a/public/components/settings-crud.js +++ b/public/components/settings-crud.js @@ -10,19 +10,12 @@ const DAYS = [ { 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" -]; +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() { @@ -31,6 +24,9 @@ class SettingsCrud extends HTMLElement { this.settings = null; this.loading = false; this.saving = false; + this.zones = []; + this.selectedZoneId = null; + this._mapEditor = null; this.shadowRoot.innerHTML = ` @@ -204,14 +180,12 @@ class SettingsCrud extends HTMLElement { try { this.settings = await api.getSettings(); - // Asegurar que schedule existe if (!this.settings.schedule) { this.settings.schedule = { delivery: {}, pickup: {} }; } - // Asegurar que delivery_zones existe - if (!this.settings.delivery_zones) { - this.settings.delivery_zones = {}; - } + 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) { @@ -275,70 +249,115 @@ 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, "_"); + ZONE_PALETTE_VARS() { + return ["--chart-blue", "--chart-green", "--chart-purple", "--chart-orange", "--chart-pink", "--chart-gray"]; } - getZoneConfig(barrioKey) { - return this.settings?.delivery_zones?.[barrioKey] || null; + zoneSwatchColor(idx) { + const palette = this.ZONE_PALETTE_VARS(); + return `var(${palette[idx % palette.length]})`; } - 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; + 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() { - 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; - + if (!this.zones.length) { + return `
No hay zonas dibujadas todavía. Tocá Crear zona y dibujá un polígono en el mapa.
`; + } + 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 ` -
-
-
- ${barrio} -
-
- ${DAYS.map(d => ` -
${d.short}
- `).join("")} -
-
- - -
-
+
+
+
+
${this.escapeHtml(z.name || z.id)}
+
${this.escapeHtml(meta || "sin configurar")}
`; }).join(""); } - renderZonesSummary() { - const zones = this.settings?.delivery_zones || {}; - const activeZones = Object.entries(zones).filter(([k, v]) => v?.enabled); - - if (activeZones.length === 0) { - return `
No hay zonas de entrega configuradas. Activá los barrios donde hacés delivery.
`; + renderZoneForm() { + const z = this.zones.find((x) => x.id === this.selectedZoneId); + if (!z) { + return `
Seleccioná una zona en la lista o dibujá una nueva en el mapa para configurarla.
`; } - - return `
${activeZones.length} zona${activeZones.length > 1 ? 's' : ''} de entrega activa${activeZones.length > 1 ? 's' : ''}
`; + 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 ` +
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+ ${DAYS.map((d) => ` + ${d.short} + `).join("")} +
+
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+ `; + } + + renderZonesSummary() { + const enabled = this.zones.filter((z) => z.enabled !== false); + if (!this.zones.length) { + return `
Dibujá al menos una zona en el mapa para que el bot pueda confirmar envíos. Sin zonas, todas las direcciones quedan sin validar.
`; + } + return `
${enabled.length} de ${this.zones.length} zona${this.zones.length > 1 ? "s" : ""} habilitada${enabled.length === 1 ? "" : "s"}.
`; } render() { @@ -434,18 +453,31 @@ class SettingsCrud extends HTMLElement {
- Zonas de Entrega (Barrios CABA) + Zonas de Entrega
- -