- Implemented rescheduleAdminBooking service to allow users to change court and time for confirmed bookings. - Added validation for court availability, maintenance status, and overlapping bookings. - Created reschedule-admin-booking handler to process rescheduling requests and send confirmation emails. - Updated booking email service to include rescheduling notifications. - Enhanced frontend components to support booking rescheduling, including a new dialog for selecting new court and time. - Added tests for rescheduling logic, covering various scenarios including validation errors and successful reschedules. - Updated Prisma schema to log previous court and time for audit purposes.
568 lines
23 KiB
TypeScript
568 lines
23 KiB
TypeScript
type BookingEmailData = {
|
|
bookingCode: string;
|
|
complexSlug?: string;
|
|
complexName: string;
|
|
date: string;
|
|
startTime: string;
|
|
endTime: string;
|
|
courtName: string;
|
|
sportName: string;
|
|
customerName: string;
|
|
price?: number;
|
|
};
|
|
|
|
const APP_BASE_URL = process.env.APP_BASE_URL ?? 'http://localhost:5173';
|
|
|
|
function formatBookingPrice(price: number): string {
|
|
if (price === 0) {
|
|
return 'Sin cargo';
|
|
}
|
|
|
|
return new Intl.NumberFormat('es-AR', {
|
|
style: 'currency',
|
|
currency: 'ARS',
|
|
maximumFractionDigits: Number.isInteger(price) ? 0 : 2,
|
|
}).format(price);
|
|
}
|
|
|
|
function formatFriendlyDate(isoDate: string): string {
|
|
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(isoDate);
|
|
if (!match) return isoDate;
|
|
|
|
const date = new Date(Number(match[1]), Number(match[2]) - 1, Number(match[3]));
|
|
|
|
const weekday = new Intl.DateTimeFormat('es-AR', { weekday: 'long' }).format(date);
|
|
const day = date.getDate();
|
|
const month = new Intl.DateTimeFormat('es-AR', { month: 'long' }).format(date);
|
|
|
|
return `${weekday.charAt(0).toUpperCase() + weekday.slice(1)}, ${day} de ${month}`;
|
|
}
|
|
|
|
const BASE_STYLES = `
|
|
body {
|
|
margin: 0;
|
|
padding: 0;
|
|
background-color: #edf7f4;
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
}
|
|
* {
|
|
box-sizing: border-box;
|
|
}
|
|
`;
|
|
|
|
export function wrapLayout(content: string) {
|
|
return `<!DOCTYPE html>
|
|
<html lang="es">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<title>Playzer</title>
|
|
<style>${BASE_STYLES}</style>
|
|
</head>
|
|
<body style="margin:0;padding:0;background-color:#edf7f4;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif;">
|
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color:#edf7f4;">
|
|
<tr>
|
|
<td align="center" style="padding:24px 12px;">
|
|
<table role="presentation" width="100%" style="max-width:520px;background-color:#ffffff;border-radius:28px;overflow:hidden;box-shadow:0 24px 70px rgba(15,23,42,0.12);border:1px solid rgba(5,9,20,0.1);">
|
|
${content}
|
|
</table>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
</body>
|
|
</html>`;
|
|
}
|
|
|
|
export function bookingConfirmationHtml(data: BookingEmailData): string {
|
|
const formattedPrice = formatBookingPrice(data.price ?? 0);
|
|
const friendlyDate = formatFriendlyDate(data.date);
|
|
|
|
const content = `
|
|
<tr>
|
|
<td style="padding:24px 20px 16px;">
|
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0">
|
|
<tr>
|
|
<td valign="top">
|
|
<table role="presentation" cellpadding="0" cellspacing="0" style="display:inline-block;background-color:#f0fdf4;border-radius:999px;padding:4px 12px;">
|
|
<tr>
|
|
<td style="font-size:11px;font-weight:700;letter-spacing:0.14em;color:#15803d;text-transform:uppercase;line-height:1.25rem;">
|
|
✓ Reserva confirmada
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
<h1 style="margin:12px 0 0;font-size:28px;font-weight:700;color:#111827;letter-spacing:-0.025em;">
|
|
${data.complexName}
|
|
</h1>
|
|
</td>
|
|
<td valign="top" align="right" style="white-space:nowrap;">
|
|
<table role="presentation" cellpadding="0" cellspacing="0">
|
|
<tr>
|
|
<td valign="middle" style="padding-right:8px;">
|
|
<img src="${APP_BASE_URL}/playzer-favicon-512-transparent.png" alt="Playzer" width="32" height="32" style="display:block;" />
|
|
</td>
|
|
<td valign="middle">
|
|
<span style="font-size:18px;font-weight:700;color:#475569;letter-spacing:-0.025em;">Playzer</span>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
</td>
|
|
</tr>
|
|
|
|
<tr>
|
|
<td style="padding:0 20px 16px;">
|
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color:#f0fdf4;border-radius:24px;border:1px solid rgba(5,150,105,0.3);">
|
|
<tr>
|
|
<td style="padding:20px 24px;">
|
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0">
|
|
<tr>
|
|
<td valign="bottom">
|
|
<p style="margin:0;font-size:13px;font-weight:600;color:#15803d;">
|
|
Tu turno
|
|
</p>
|
|
<p style="margin:12px 0 0;font-size:28px;font-weight:900;color:#111827;line-height:1.1;letter-spacing:-0.025em;">
|
|
${friendlyDate}
|
|
</p>
|
|
<p style="margin:8px 0 0;font-size:24px;font-weight:900;color:#059669;line-height:1;letter-spacing:-0.025em;">
|
|
${data.startTime} — ${data.endTime}
|
|
</p>
|
|
</td>
|
|
<td valign="bottom" align="right" style="padding-left:16px;">
|
|
<table role="presentation" cellpadding="0" cellspacing="0" style="background-color:rgba(255,255,255,0.8);border-radius:16px;border:1px solid rgba(5,150,105,0.2);">
|
|
<tr>
|
|
<td style="padding:12px 16px;text-align:right;min-width:120px;">
|
|
<p style="margin:0;font-size:11px;font-weight:500;color:#6b7280;">Precio del turno</p>
|
|
<p style="margin:4px 0 0;font-size:20px;font-weight:700;color:#111827;">${formattedPrice}</p>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
</td>
|
|
</tr>
|
|
|
|
<tr>
|
|
<td style="padding:0 20px 16px;">
|
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="border:1px solid #e5e7eb;border-radius:22px;overflow:hidden;">
|
|
<tr>
|
|
<td width="50%" style="padding:16px;border-right:1px solid #e5e7eb;background-color:#ffffff;">
|
|
<p style="margin:0;font-size:13px;font-weight:500;color:#6b7280;">
|
|
Cancha
|
|
</p>
|
|
<p style="margin:8px 0 0;font-size:14px;font-weight:600;color:#111827;">
|
|
${data.courtName} — ${data.sportName}
|
|
</p>
|
|
</td>
|
|
<td width="50%" style="padding:16px;background-color:#ffffff;">
|
|
<p style="margin:0;font-size:13px;font-weight:500;color:#6b7280;">
|
|
Código de reserva
|
|
</p>
|
|
<p style="margin:8px 0 0;font-family:monospace;font-size:16px;font-weight:700;letter-spacing:0.18em;color:#111827;">
|
|
${data.bookingCode}
|
|
</p>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
</td>
|
|
</tr>
|
|
|
|
${
|
|
data.complexSlug
|
|
? `
|
|
<tr>
|
|
<td style="padding:0 20px 24px;">
|
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0">
|
|
<tr>
|
|
<td align="center">
|
|
<table role="presentation" cellpadding="0" cellspacing="0" style="background-color:#059669;border-radius:12px;">
|
|
<tr>
|
|
<td style="padding:14px 32px;font-size:15px;font-weight:600;">
|
|
<a href="${APP_BASE_URL}/${data.complexSlug}/booking/confirmed/${data.bookingCode}" style="color:#ffffff;text-decoration:none;display:inline-block;">
|
|
Cancelar reserva
|
|
</a>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
</td>
|
|
</tr>`
|
|
: ''
|
|
}`;
|
|
|
|
return wrapLayout(content);
|
|
}
|
|
|
|
export function bookingCancelledHtml(data: BookingEmailData): string {
|
|
const friendlyDate = formatFriendlyDate(data.date);
|
|
|
|
const content = `
|
|
<tr>
|
|
<td style="padding:24px 20px 16px;">
|
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0">
|
|
<tr>
|
|
<td valign="top">
|
|
<table role="presentation" cellpadding="0" cellspacing="0" style="display:inline-block;background-color:#fef2f2;border-radius:999px;padding:4px 12px;">
|
|
<tr>
|
|
<td style="font-size:11px;font-weight:700;letter-spacing:0.14em;color:#dc2626;text-transform:uppercase;line-height:1.25rem;">
|
|
✗ Reserva cancelada
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
<h1 style="margin:12px 0 0;font-size:28px;font-weight:700;color:#111827;letter-spacing:-0.025em;">
|
|
${data.complexName}
|
|
</h1>
|
|
</td>
|
|
<td valign="top" align="right" style="white-space:nowrap;">
|
|
<table role="presentation" cellpadding="0" cellspacing="0">
|
|
<tr>
|
|
<td valign="middle" style="padding-right:8px;">
|
|
<img src="${APP_BASE_URL}/playzer-favicon-512-transparent.png" alt="Playzer" width="32" height="32" style="display:block;" />
|
|
</td>
|
|
<td valign="middle">
|
|
<span style="font-size:18px;font-weight:700;color:#475569;letter-spacing:-0.025em;">Playzer</span>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
</td>
|
|
</tr>
|
|
|
|
<tr>
|
|
<td style="padding:0 20px 16px;">
|
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color:#f0fdf4;border-radius:24px;border:1px solid rgba(5,150,105,0.3);">
|
|
<tr>
|
|
<td style="padding:20px 24px;">
|
|
<p style="margin:0;font-size:13px;font-weight:600;color:#15803d;">
|
|
Tu turno
|
|
</p>
|
|
<p style="margin:12px 0 0;font-size:28px;font-weight:900;color:#111827;line-height:1.1;letter-spacing:-0.025em;">
|
|
${friendlyDate}
|
|
</p>
|
|
<p style="margin:8px 0 0;font-size:24px;font-weight:900;color:#059669;line-height:1;letter-spacing:-0.025em;">
|
|
${data.startTime} — ${data.endTime}
|
|
</p>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
</td>
|
|
</tr>
|
|
|
|
<tr>
|
|
<td style="padding:0 20px 16px;">
|
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color:#fef2f2;border-radius:24px;border:1px solid rgba(220,38,38,0.3);">
|
|
<tr>
|
|
<td style="padding:20px 24px;">
|
|
<p style="margin:0;font-size:14px;color:#374151;line-height:1.6;">
|
|
La reserva <strong style="font-family:monospace;font-weight:700;letter-spacing:2px;">${data.bookingCode}</strong>
|
|
fue cancelada. Si tenés dudas, contactate con el complejo.
|
|
</p>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
</td>
|
|
</tr>
|
|
|
|
<tr>
|
|
<td style="padding:0 20px 24px;">
|
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="border:1px solid #e5e7eb;border-radius:22px;overflow:hidden;">
|
|
<tr>
|
|
<td width="50%" style="padding:16px;border-right:1px solid #e5e7eb;background-color:#ffffff;">
|
|
<p style="margin:0;font-size:13px;font-weight:500;color:#6b7280;">
|
|
Cancha
|
|
</p>
|
|
<p style="margin:8px 0 0;font-size:14px;font-weight:600;color:#111827;">
|
|
${data.courtName} — ${data.sportName}
|
|
</p>
|
|
</td>
|
|
<td width="50%" style="padding:16px;background-color:#ffffff;">
|
|
<p style="margin:0;font-size:13px;font-weight:500;color:#6b7280;">
|
|
Cliente
|
|
</p>
|
|
<p style="margin:8px 0 0;font-size:14px;font-weight:600;color:#111827;">
|
|
${data.customerName}
|
|
</p>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
</td>
|
|
</tr>`;
|
|
|
|
return wrapLayout(content);
|
|
}
|
|
|
|
type BookingRescheduledEmailData = BookingEmailData & {
|
|
previousCourtName: string;
|
|
previousStartTime: string;
|
|
previousEndTime: string;
|
|
};
|
|
|
|
export function bookingRescheduledHtml(data: BookingRescheduledEmailData): string {
|
|
const friendlyDate = formatFriendlyDate(data.date);
|
|
|
|
const content = `
|
|
<tr>
|
|
<td style="padding:24px 20px 16px;">
|
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0">
|
|
<tr>
|
|
<td valign="top">
|
|
<table role="presentation" cellpadding="0" cellspacing="0" style="display:inline-block;background-color:#f4f4f5;border-radius:999px;padding:4px 12px;">
|
|
<tr>
|
|
<td style="font-size:11px;font-weight:700;letter-spacing:0.14em;color:#71717a;text-transform:uppercase;line-height:1.25rem;">
|
|
Reserva reprogramada
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
<h1 style="margin:12px 0 0;font-size:28px;font-weight:700;color:#111827;letter-spacing:-0.025em;">
|
|
${data.complexName}
|
|
</h1>
|
|
</td>
|
|
<td valign="top" align="right" style="white-space:nowrap;">
|
|
<table role="presentation" cellpadding="0" cellspacing="0">
|
|
<tr>
|
|
<td valign="middle" style="padding-right:8px;">
|
|
<img src="${APP_BASE_URL}/playzer-favicon-512-transparent.png" alt="Playzer" width="32" height="32" style="display:block;" />
|
|
</td>
|
|
<td valign="middle">
|
|
<span style="font-size:18px;font-weight:700;color:#475569;letter-spacing:-0.025em;">Playzer</span>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
</td>
|
|
</tr>
|
|
|
|
<tr>
|
|
<td style="padding:0 20px 16px;">
|
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color:#f0fdf4;border-radius:24px;border:1px solid rgba(5,150,105,0.3);">
|
|
<tr>
|
|
<td style="padding:20px 24px;">
|
|
<p style="margin:0;font-size:13px;font-weight:600;color:#15803d;">
|
|
Nuevo turno
|
|
</p>
|
|
<p style="margin:12px 0 0;font-size:28px;font-weight:900;color:#111827;line-height:1.1;letter-spacing:-0.025em;">
|
|
${friendlyDate}
|
|
</p>
|
|
<p style="margin:8px 0 0;font-size:24px;font-weight:900;color:#059669;line-height:1;letter-spacing:-0.025em;">
|
|
${data.startTime} — ${data.endTime}
|
|
</p>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
</td>
|
|
</tr>
|
|
|
|
<tr>
|
|
<td style="padding:0 20px 16px;">
|
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="border:1px solid #e5e7eb;border-radius:22px;overflow:hidden;">
|
|
<tr>
|
|
<td width="50%" style="padding:16px;border-right:1px solid #e5e7eb;background-color:#ffffff;">
|
|
<p style="margin:0;font-size:13px;font-weight:500;color:#6b7280;">
|
|
Cancha anterior
|
|
</p>
|
|
<p style="margin:8px 0 0;font-size:14px;font-weight:600;color:#111827;">
|
|
${data.previousCourtName} — ${data.sportName}
|
|
</p>
|
|
</td>
|
|
<td width="50%" style="padding:16px;background-color:#ffffff;">
|
|
<p style="margin:0;font-size:13px;font-weight:500;color:#6b7280;">
|
|
Cancha nueva
|
|
</p>
|
|
<p style="margin:8px 0 0;font-size:14px;font-weight:600;color:#111827;">
|
|
${data.courtName} — ${data.sportName}
|
|
</p>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
</td>
|
|
</tr>
|
|
|
|
<tr>
|
|
<td style="padding:0 20px 16px;">
|
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="border:1px solid #e5e7eb;border-radius:22px;overflow:hidden;">
|
|
<tr>
|
|
<td width="50%" style="padding:16px;border-right:1px solid #e5e7eb;background-color:#ffffff;">
|
|
<p style="margin:0;font-size:13px;font-weight:500;color:#6b7280;">
|
|
Horario anterior
|
|
</p>
|
|
<p style="margin:8px 0 0;font-size:14px;font-weight:600;color:#111827;">
|
|
${data.previousStartTime} — ${data.previousEndTime}
|
|
</p>
|
|
</td>
|
|
<td width="50%" style="padding:16px;background-color:#ffffff;">
|
|
<p style="margin:0;font-size:13px;font-weight:500;color:#6b7280;">
|
|
Horario nuevo
|
|
</p>
|
|
<p style="margin:8px 0 0;font-size:14px;font-weight:600;color:#111827;">
|
|
${data.startTime} — ${data.endTime}
|
|
</p>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
</td>
|
|
</tr>
|
|
|
|
<tr>
|
|
<td style="padding:0 20px 16px;">
|
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="border:1px solid #e5e7eb;border-radius:22px;overflow:hidden;">
|
|
<tr>
|
|
<td width="50%" style="padding:16px;border-right:1px solid #e5e7eb;background-color:#ffffff;">
|
|
<p style="margin:0;font-size:13px;font-weight:500;color:#6b7280;">
|
|
Cliente
|
|
</p>
|
|
<p style="margin:8px 0 0;font-size:14px;font-weight:600;color:#111827;">
|
|
${data.customerName}
|
|
</p>
|
|
</td>
|
|
<td width="50%" style="padding:16px;background-color:#ffffff;">
|
|
<p style="margin:0;font-size:13px;font-weight:500;color:#6b7280;">
|
|
Código de reserva
|
|
</p>
|
|
<p style="margin:8px 0 0;font-family:monospace;font-size:16px;font-weight:700;letter-spacing:0.18em;color:#111827;">
|
|
${data.bookingCode}
|
|
</p>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
</td>
|
|
</tr>
|
|
|
|
${
|
|
data.complexSlug
|
|
? `
|
|
<tr>
|
|
<td style="padding:0 20px 24px;">
|
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0">
|
|
<tr>
|
|
<td align="center">
|
|
<table role="presentation" cellpadding="0" cellspacing="0" style="background-color:#059669;border-radius:12px;">
|
|
<tr>
|
|
<td style="padding:14px 32px;font-size:15px;font-weight:600;">
|
|
<a href="${APP_BASE_URL}/${data.complexSlug}/booking/confirmed/${data.bookingCode}" style="color:#ffffff;text-decoration:none;display:inline-block;">
|
|
Ver reserva
|
|
</a>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
</td>
|
|
</tr>`
|
|
: ''
|
|
}`;
|
|
|
|
return wrapLayout(content);
|
|
}
|
|
|
|
export function bookingNoShowHtml(data: BookingEmailData): string {
|
|
const friendlyDate = formatFriendlyDate(data.date);
|
|
|
|
const content = `
|
|
<tr>
|
|
<td style="padding:24px 20px 16px;">
|
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0">
|
|
<tr>
|
|
<td valign="top">
|
|
<table role="presentation" cellpadding="0" cellspacing="0" style="display:inline-block;background-color:#fffbeb;border-radius:999px;padding:4px 12px;">
|
|
<tr>
|
|
<td style="font-size:11px;font-weight:700;letter-spacing:0.14em;color:#d97706;text-transform:uppercase;line-height:1.25rem;">
|
|
Reserva no concretada
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
<h1 style="margin:12px 0 0;font-size:28px;font-weight:700;color:#111827;letter-spacing:-0.025em;">
|
|
${data.complexName}
|
|
</h1>
|
|
</td>
|
|
<td valign="top" align="right" style="white-space:nowrap;">
|
|
<table role="presentation" cellpadding="0" cellspacing="0">
|
|
<tr>
|
|
<td valign="middle" style="padding-right:8px;">
|
|
<img src="${APP_BASE_URL}/playzer-favicon-512-transparent.png" alt="Playzer" width="32" height="32" style="display:block;" />
|
|
</td>
|
|
<td valign="middle">
|
|
<span style="font-size:18px;font-weight:700;color:#475569;letter-spacing:-0.025em;">Playzer</span>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
</td>
|
|
</tr>
|
|
|
|
<tr>
|
|
<td style="padding:0 20px 16px;">
|
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color:#f0fdf4;border-radius:24px;border:1px solid rgba(5,150,105,0.3);">
|
|
<tr>
|
|
<td style="padding:20px 24px;">
|
|
<p style="margin:0;font-size:13px;font-weight:600;color:#15803d;">
|
|
Tu turno
|
|
</p>
|
|
<p style="margin:12px 0 0;font-size:28px;font-weight:900;color:#111827;line-height:1.1;letter-spacing:-0.025em;">
|
|
${friendlyDate}
|
|
</p>
|
|
<p style="margin:8px 0 0;font-size:24px;font-weight:900;color:#059669;line-height:1;letter-spacing:-0.025em;">
|
|
${data.startTime} — ${data.endTime}
|
|
</p>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
</td>
|
|
</tr>
|
|
|
|
<tr>
|
|
<td style="padding:0 20px 16px;">
|
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color:#fffbeb;border-radius:24px;border:1px solid rgba(217,119,6,0.3);">
|
|
<tr>
|
|
<td style="padding:20px 24px;">
|
|
<p style="margin:0;font-size:14px;color:#374151;line-height:1.6;">
|
|
La reserva <strong style="font-family:monospace;font-weight:700;letter-spacing:2px;">${data.bookingCode}</strong>
|
|
fue registrada como no concretada por falta de asistencia.
|
|
</p>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
</td>
|
|
</tr>
|
|
|
|
<tr>
|
|
<td style="padding:0 20px 24px;">
|
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="border:1px solid #e5e7eb;border-radius:22px;overflow:hidden;">
|
|
<tr>
|
|
<td width="50%" style="padding:16px;border-right:1px solid #e5e7eb;background-color:#ffffff;">
|
|
<p style="margin:0;font-size:13px;font-weight:500;color:#6b7280;">
|
|
Cancha
|
|
</p>
|
|
<p style="margin:8px 0 0;font-size:14px;font-weight:600;color:#111827;">
|
|
${data.courtName} — ${data.sportName}
|
|
</p>
|
|
</td>
|
|
<td width="50%" style="padding:16px;background-color:#ffffff;">
|
|
<p style="margin:0;font-size:13px;font-weight:500;color:#6b7280;">
|
|
Cliente
|
|
</p>
|
|
<p style="margin:8px 0 0;font-size:14px;font-weight:600;color:#111827;">
|
|
${data.customerName}
|
|
</p>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
</td>
|
|
</tr>`;
|
|
|
|
return wrapLayout(content);
|
|
}
|