Backend cleanup (todo el delivery vive ahora en delivery_zones.zones[]):
- Migration drop columns delivery_enabled / delivery_days / delivery_hours_start /
delivery_hours_end / delivery_min_order y limpieza de schedule.delivery JSONB.
- settingsRepo: SELECT/INSERT/UPDATE sólo con campos vigentes, formatScheduleHours
trabaja sobre pickup.
- handlers/settings: defaults sin legacy, validateSchedule sólo para pickup,
validateDeliveryZones nuevo (estructura GeoJSON + días).
- seed_piaf_settings_and_replies + tenant_settings migrations alineadas: schedule
sólo tiene pickup, delivery_zones queda en {} para reconfigurar via UI.
Frontend cleanup:
- settings-crud: borrado el panel "Delivery (Envío a domicilio)" + minOrder,
toggle deliveryEnabled, y el grid schedule.delivery. collectScheduleFromInputs
ahora sólo procesa pickup. save() ya no envía delivery_enabled/min_order.
Fix mapa (no cargaba):
- zone-map-editor: los <link> a leaflet.css/leaflet-geoman.css se inyectaban en
document.head, que NO cruza el shadow DOM de settings-crud, por lo que las
reglas de Leaflet no aplicaban al div del mapa. Ahora los <link> se anclan
como hijos del propio web component; al estar en light DOM dentro del shadow
root del padre, sí aplican.
- Espera explícita a que el stylesheet cargue antes de instanciar L.map.
- ResizeObserver + invalidateSize() para cuando el contenedor cambia tamaño
(router muestra/oculta panel, tabs, etc).
Smoke E2E sin regresión: 1kg vacío + envío → location en Centro → "martes 12hs"
→ orden confirmada con $28.000 (producto + envío). 157/157 tests verde.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
156 lines
5.3 KiB
JavaScript
156 lines
5.3 KiB
JavaScript
import { getSettings, upsertSettings, getStoreConfig } from "../db/settingsRepo.js";
|
|
|
|
const VALID_DAYS = ["lun", "mar", "mie", "jue", "vie", "sab", "dom"];
|
|
|
|
function defaultPickupSchedule() {
|
|
const days = ["lun", "mar", "mie", "jue", "vie", "sab"];
|
|
const pickup = {};
|
|
for (const d of days) pickup[d] = { start: "08:00", end: "20:00" };
|
|
return { pickup };
|
|
}
|
|
|
|
export async function handleGetSettings({ tenantId }) {
|
|
const settings = await getSettings({ tenantId });
|
|
|
|
if (!settings) {
|
|
return {
|
|
store_name: "Mi Negocio",
|
|
bot_name: "Piaf",
|
|
store_address: "",
|
|
store_phone: "",
|
|
pickup_enabled: true,
|
|
pickup_days: "lun,mar,mie,jue,vie,sab",
|
|
pickup_hours_start: "08:00",
|
|
pickup_hours_end: "20:00",
|
|
schedule: defaultPickupSchedule(),
|
|
delivery_zones: {},
|
|
is_default: true,
|
|
};
|
|
}
|
|
|
|
// Si schedule está vacío pero tenemos los campos legacy de pickup, generar
|
|
// schedule.pickup para que la UI pueda editar el grid por día.
|
|
let schedule = settings.schedule;
|
|
if (!schedule || !schedule.pickup || !Object.keys(schedule.pickup).length) {
|
|
schedule = buildPickupScheduleFromLegacy(settings);
|
|
}
|
|
|
|
return {
|
|
...settings,
|
|
pickup_hours_start: settings.pickup_hours_start?.slice(0, 5) || "08:00",
|
|
pickup_hours_end: settings.pickup_hours_end?.slice(0, 5) || "20:00",
|
|
schedule,
|
|
delivery_zones: settings.delivery_zones || {},
|
|
is_default: false,
|
|
};
|
|
}
|
|
|
|
function buildPickupScheduleFromLegacy(settings) {
|
|
const out = { pickup: {} };
|
|
if (settings?.pickup_enabled && settings?.pickup_days) {
|
|
const days = settings.pickup_days.split(",").map((d) => d.trim());
|
|
const start = settings.pickup_hours_start?.slice(0, 5) || "08:00";
|
|
const end = settings.pickup_hours_end?.slice(0, 5) || "20:00";
|
|
for (const day of days) {
|
|
if (VALID_DAYS.includes(day)) out.pickup[day] = { start, end };
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function validateSchedule(schedule) {
|
|
if (!schedule || typeof schedule !== "object") return;
|
|
const timeRegex = /^([01]?[0-9]|2[0-3]):[0-5][0-9](:[0-5][0-9])?$/;
|
|
const pickup = schedule.pickup;
|
|
if (!pickup || typeof pickup !== "object") return;
|
|
for (const [day, slot] of Object.entries(pickup)) {
|
|
if (!VALID_DAYS.includes(day)) throw new Error(`Invalid day in schedule.pickup: ${day}`);
|
|
if (slot === null) continue;
|
|
if (typeof slot !== "object" || !slot.start || !slot.end) {
|
|
throw new Error(`Invalid slot format for pickup.${day}`);
|
|
}
|
|
if (!timeRegex.test(slot.start)) throw new Error(`Invalid start time for pickup.${day}: ${slot.start}`);
|
|
if (!timeRegex.test(slot.end)) throw new Error(`Invalid end time for pickup.${day}: ${slot.end}`);
|
|
}
|
|
}
|
|
|
|
function syncPickupLegacyFromSchedule(settings) {
|
|
const pickup = settings?.schedule?.pickup;
|
|
if (!pickup) return;
|
|
const days = Object.keys(pickup).filter((d) => pickup[d] && pickup[d].start && pickup[d].end);
|
|
days.sort((a, b) => VALID_DAYS.indexOf(a) - VALID_DAYS.indexOf(b));
|
|
if (days.length) {
|
|
settings.pickup_days = days.join(",");
|
|
const first = pickup[days[0]];
|
|
settings.pickup_hours_start = first.start;
|
|
settings.pickup_hours_end = first.end;
|
|
} else {
|
|
settings.pickup_days = "";
|
|
}
|
|
}
|
|
|
|
function validateDeliveryZones(dz) {
|
|
if (!dz || typeof dz !== "object") return;
|
|
if (dz.zones && !Array.isArray(dz.zones)) {
|
|
throw new Error("delivery_zones.zones must be an array");
|
|
}
|
|
for (const z of dz.zones || []) {
|
|
if (!z?.id || !z?.name) throw new Error("Each zone needs id + name");
|
|
if (z.polygon && (z.polygon.type !== "Polygon" || !Array.isArray(z.polygon.coordinates))) {
|
|
throw new Error(`Invalid polygon GeoJSON for zone ${z.id}`);
|
|
}
|
|
if (Array.isArray(z.delivery_days)) {
|
|
for (const d of z.delivery_days) {
|
|
if (!VALID_DAYS.includes(d)) throw new Error(`Invalid delivery day in zone ${z.id}: ${d}`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
export async function handleSaveSettings({ tenantId, settings }) {
|
|
if (!settings.store_name?.trim()) throw new Error("store_name is required");
|
|
if (!settings.bot_name?.trim()) throw new Error("bot_name is required");
|
|
|
|
if (settings.schedule) {
|
|
validateSchedule(settings.schedule);
|
|
syncPickupLegacyFromSchedule(settings);
|
|
}
|
|
|
|
if (settings.delivery_zones) {
|
|
validateDeliveryZones(settings.delivery_zones);
|
|
}
|
|
|
|
if (settings.pickup_days) {
|
|
const days = settings.pickup_days.split(",").map((d) => d.trim().toLowerCase());
|
|
for (const d of days) {
|
|
if (!VALID_DAYS.includes(d)) throw new Error(`Invalid pickup day: ${d}`);
|
|
}
|
|
settings.pickup_days = days.join(",");
|
|
}
|
|
|
|
const timeRegex = /^([01]?[0-9]|2[0-3]):[0-5][0-9]$/;
|
|
if (settings.pickup_hours_start && !timeRegex.test(settings.pickup_hours_start)) {
|
|
throw new Error("Invalid pickup_hours_start format (use HH:MM)");
|
|
}
|
|
if (settings.pickup_hours_end && !timeRegex.test(settings.pickup_hours_end)) {
|
|
throw new Error("Invalid pickup_hours_end format (use HH:MM)");
|
|
}
|
|
|
|
const result = await upsertSettings({ tenantId, settings });
|
|
|
|
return {
|
|
ok: true,
|
|
settings: {
|
|
...result,
|
|
pickup_hours_start: result.pickup_hours_start?.slice(0, 5),
|
|
pickup_hours_end: result.pickup_hours_end?.slice(0, 5),
|
|
delivery_zones: result.delivery_zones || {},
|
|
},
|
|
message: "Configuración guardada correctamente",
|
|
};
|
|
}
|
|
|
|
export async function handleGetStoreConfig({ tenantId }) {
|
|
return await getStoreConfig({ tenantId });
|
|
}
|