- Reservas: campos opcionales start_time/end_time (migración 011, schema natur_reservas) + toggle en el modal y detección de solapamiento por horario cuando ambas reservas los tienen definidos. Permite encajar varios eventos el mismo día. - Calendario mensual y anual ahora empiezan en lunes; vista móvil incluida. - Celdas con varios eventos el mismo día se dividen en franjas horizontales mostrando el horario; las reservas multi-día siguen ocupando la celda completa. - Modal: reset de campos vacíos (client_name, fechas, factura) para evitar que el nombre de la última reserva se filtre al crear una nueva. - Emails: las modificaciones solo disparan correo cuando cambian fechas u horas; el correo a Teneriffa pasa a formato reducido (solo fechas + propiedad) mientras que Natur sigue recibiendo el detalle completo. Mantenimiento sin cambios. - CLAUDE.md con guía operativa (schema natur_reservas, stack, convenciones). - Scripts de preview/envío de emails para pruebas. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
51 lines
2.6 KiB
JavaScript
51 lines
2.6 KiB
JavaScript
// Envía 3 ejemplos del correo minimalista de Teneriffa: nueva, modificada, cancelada.
|
||
// Uso: N8N_EMAIL_WEBHOOK_URL=... node apps/api/scripts/send-test-teneriffa-variants.mjs <destino>
|
||
|
||
const WEBHOOK = process.env.N8N_EMAIL_WEBHOOK_URL;
|
||
const FROM = process.env.EMAIL_FROM ?? 'Naturcalabacera <reservas@naturcalabacera.com>';
|
||
const TO = process.argv[2] ?? 'kilian.parraga@gmail.com';
|
||
|
||
if (!WEBHOOK) { console.error('Falta N8N_EMAIL_WEBHOOK_URL'); process.exit(1); }
|
||
|
||
function renderTeneriffaMinimal(actionLabel, dateRange, property, cancelled = false) {
|
||
const accent = cancelled ? '#ef4444' : actionLabel === 'Nueva Reserva' ? '#3b82f6' : '#f59e0b';
|
||
return `<!DOCTYPE html>
|
||
<html lang="es"><head><meta charset="UTF-8"></head>
|
||
<body style="font-family:Arial,sans-serif;background:#f4f4f4;margin:0;padding:24px;">
|
||
<div style="max-width:520px;margin:0 auto;background:#fff;border-radius:12px;overflow:hidden;box-shadow:0 2px 8px rgba(0,0,0,0.08);">
|
||
<div style="background:${accent};color:#fff;padding:20px 24px;">
|
||
<p style="margin:0;font-size:11px;text-transform:uppercase;letter-spacing:0.1em;opacity:0.85;font-weight:700;">${actionLabel}</p>
|
||
</div>
|
||
<div style="padding:24px;">
|
||
<p style="margin:0 0 6px;font-size:11px;text-transform:uppercase;letter-spacing:0.06em;color:#9ca3af;font-weight:700;">Fechas</p>
|
||
<p style="margin:0 0 18px;font-size:18px;font-weight:800;color:#111;">${dateRange}</p>
|
||
<p style="margin:0 0 6px;font-size:11px;text-transform:uppercase;letter-spacing:0.06em;color:#9ca3af;font-weight:700;">Propiedad</p>
|
||
<p style="margin:0;font-size:16px;font-weight:700;color:#111;">${property}</p>
|
||
</div>
|
||
</div>
|
||
</body></html>`;
|
||
}
|
||
|
||
async function send(subject, html) {
|
||
const res = await fetch(WEBHOOK, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ to: TO, subject, html, from: FROM }),
|
||
});
|
||
const text = await res.text().catch(() => '');
|
||
return { ok: res.ok, status: res.status, body: text.slice(0, 200) };
|
||
}
|
||
|
||
const cases = [
|
||
{ tag: 'NUEVA RESERVA', label: 'Nueva Reserva', range: '03 jul 2026 – 10 jul 2026', prop: 'La Esquinita' },
|
||
{ tag: 'MODIFICADA', label: 'Reserva Modificada', range: '03 jul 2026 – 12 jul 2026', prop: 'La Esquinita' },
|
||
{ tag: 'CANCELADA', label: 'Reserva Cancelada', range: '03 jul 2026 – 12 jul 2026', prop: 'La Esquinita', cancelled: true },
|
||
];
|
||
|
||
for (const c of cases) {
|
||
const subject = `[${c.tag}] ${c.range} · ${c.prop}`;
|
||
const html = renderTeneriffaMinimal(c.label, c.range, c.prop, c.cancelled);
|
||
const r = await send(subject, html);
|
||
console.log(c.tag.padEnd(15), r);
|
||
}
|