Files
botino/public/components/settings-crud.js
Lucas Tettamanti 5e79f17d00 20260204
2026-02-04 16:06:51 -03:00

674 lines
26 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" },
];
// 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 {
constructor() {
super();
this.attachShadow({ mode: "open" });
this.settings = null;
this.loading = false;
this.saving = false;
this.shadowRoot.innerHTML = `
<style>
:host { display:block; height:100%; padding:16px; overflow:auto; }
* { box-sizing:border-box; font-family:system-ui,Segoe UI,Roboto,Arial; }
.container { max-width:800px; margin:0 auto; }
.panel { background:#121823; border:1px solid #1e2a3a; border-radius:10px; padding:20px; margin-bottom:16px; }
.panel-title { font-size:16px; font-weight:700; color:#e7eef7; margin-bottom:16px; display:flex; align-items:center; gap:8px; }
.panel-title svg { width:20px; height:20px; fill:#1f6feb; }
.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:#8aa0b5; margin-bottom:6px; text-transform:uppercase; letter-spacing:.4px; }
.field-hint { font-size:11px; color:#6c7a89; margin-top:4px; }
input, select, textarea {
background:#0f1520; color:#e7eef7; border:1px solid #253245;
border-radius:8px; padding:10px 14px; font-size:14px; width:100%;
}
input:focus, select:focus, textarea:focus { outline:none; border-color:#1f6feb; }
input:disabled { opacity:.6; cursor:not-allowed; }
button {
cursor:pointer; background:#1f6feb; color:#fff; border:none;
border-radius:8px; padding:10px 20px; font-size:14px; font-weight:600;
}
button:hover { background:#1a5fd0; }
button:disabled { opacity:.5; cursor:not-allowed; }
button.secondary { background:#253245; }
button.secondary:hover { background:#2d3e52; }
.toggle-row { display:flex; align-items:center; gap:12px; margin-bottom:16px; }
.toggle {
position:relative; width:48px; height:26px;
background:#253245; border-radius:13px; cursor:pointer;
transition:background .2s; flex-shrink:0;
}
.toggle.active { background:#1f6feb; }
.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:#e7eef7; }
/* 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:#0f1520;
border-radius:8px;
border:1px solid #1e2a3a;
}
.schedule-row.disabled { opacity:0.4; }
.day-label { font-size:13px; color:#e7eef7; font-weight:500; }
.day-toggle {
width:32px; height:18px; background:#253245; border-radius:9px;
cursor:pointer; position:relative; transition:background .2s;
}
.day-toggle.active { background:#2ecc71; }
.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:#6c7a89; 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:#8aa0b5; }
.success-msg {
background:#2ecc7130; border:1px solid #2ecc71;
color:#2ecc71; padding:12px 16px; border-radius:8px;
margin-bottom:16px; font-size:14px;
}
.error-msg {
background:#e74c3c30; border:1px solid #e74c3c;
color:#e74c3c; 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 #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">
<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();
// 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 = {};
}
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("");
}
// 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");
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 = `
<!-- Info del Negocio -->
<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="form-row">
<div class="field">
<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">
<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>
<div class="form-row">
<div class="field">
<label>Dirección</label>
<input type="text" id="storeAddress" value="${this.escapeHtml(s.store_address || "")}" placeholder="Ej: Av. Corrientes 1234, CABA" />
</div>
<div class="field">
<label>Teléfono</label>
<input type="text" id="storePhone" value="${this.escapeHtml(s.store_phone || "")}" placeholder="Ej: +54 11 1234-5678" />
</div>
</div>
</div>
<!-- Delivery -->
<div class="panel">
<div class="panel-title">
<svg viewBox="0 0 24 24"><path d="M18.92 6.01C18.72 5.42 18.16 5 17.5 5h-11c-.66 0-1.21.42-1.42 1.01L3 12v8c0 .55.45 1 1 1h1c.55 0 1-.45 1-1v-1h12v1c0 .55.45 1 1 1h1c.55 0 1-.45 1-1v-8l-2.08-5.99zM6.5 16c-.83 0-1.5-.67-1.5-1.5S5.67 13 6.5 13s1.5.67 1.5 1.5S7.33 16 6.5 16zm11 0c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zM5 11l1.5-4.5h11L19 11H5z"/></svg>
Delivery (Envío a domicilio)
</div>
<div class="toggle-row">
<div class="toggle ${s.delivery_enabled ? "active" : ""}" id="deliveryToggle"></div>
<span class="toggle-label">Delivery habilitado</span>
</div>
<div class="schedule-grid" id="deliverySchedule">
${this.renderScheduleGrid("delivery", s.delivery_enabled)}
</div>
<div class="min-order-field">
<div class="field">
<label>Pedido mínimo para delivery ($)</label>
<input type="number" id="deliveryMinOrder" value="${s.delivery_min_order || 0}" min="0" step="100" style="width:150px;" />
</div>
</div>
</div>
<!-- Retiro en tienda -->
<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 en tienda habilitado</span>
</div>
<div class="schedule-grid" id="pickupSchedule">
${this.renderScheduleGrid("pickup", s.pickup_enabled)}
</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>
</div>
`;
this.setupEventListeners();
}
setupEventListeners() {
// Toggle delivery
const deliveryToggle = this.shadowRoot.getElementById("deliveryToggle");
deliveryToggle?.addEventListener("click", () => {
this.settings.delivery_enabled = !this.settings.delivery_enabled;
this.render();
});
// 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());
// 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() {
const schedule = { delivery: {}, pickup: {} };
for (const type of ["delivery", "pickup"]) {
this.shadowRoot.querySelectorAll(`.hour-start[data-type="${type}"]`).forEach(input => {
const day = input.dataset.day;
const endInput = this.shadowRoot.querySelector(`.hour-end[data-type="${type}"][data-day="${day}"]`);
const toggle = this.shadowRoot.querySelector(`.day-toggle[data-type="${type}"][data-day="${day}"]`);
if (toggle?.classList.contains("active")) {
schedule[type][day] = {
start: input.value.trim() || (type === "delivery" ? "09:00" : "08:00"),
end: endInput?.value.trim() || (type === "delivery" ? "18:00" : "20:00"),
};
}
});
}
return schedule;
}
async save() {
// 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 || "",
store_address: this.shadowRoot.getElementById("storeAddress")?.value || "",
store_phone: this.shadowRoot.getElementById("storePhone")?.value || "",
delivery_enabled: this.settings.delivery_enabled,
pickup_enabled: this.settings.pickup_enabled,
delivery_min_order: parseFloat(this.shadowRoot.getElementById("deliveryMinOrder")?.value) || 0,
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);