feat: add reschedule functionality for admin bookings
- 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.
This commit is contained in:
@@ -300,6 +300,172 @@ export function bookingCancelledHtml(data: BookingEmailData): string {
|
||||
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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user