feat: add customer email functionality for booking confirmations

- Implemented email confirmation for admin and public bookings.
- Added customerEmail field to booking schemas and services.
- Created email templates for booking confirmation, cancellation, and no-show notifications.
- Updated booking handlers to send emails upon booking creation and status updates.
- Enhanced frontend forms to capture customer email during booking creation.
This commit is contained in:
Jose Selesan
2026-06-02 19:21:04 -03:00
parent 50fa4ed9a5
commit 85f234b05e
22 changed files with 610 additions and 1536 deletions

View File

@@ -41,6 +41,7 @@ interface CreateManualBookingPayload {
startTime: string;
customerName: string;
customerPhone: string;
customerEmail?: string;
}
interface BookingContextValue {
@@ -315,6 +316,7 @@ export function BookingProvider({ children, complex }: BookingProviderProps) {
startTime: payload.startTime,
customerName: payload.customerName,
customerPhone: payload.customerPhone,
customerEmail: payload.customerEmail || undefined,
}),
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: ['admin-bookings', complex.id] });

View File

@@ -42,6 +42,7 @@ const bookingFormSchema = z.object({
.trim()
.min(6, 'Ingresa un telefono válido.')
.max(30, 'El telefono no puede superar los 30 caracteres.'),
customerEmail: z.string().email('Ingresá un email válido.').optional().or(z.literal('')),
});
type BookingForm = z.infer<typeof bookingFormSchema>;
@@ -76,6 +77,7 @@ export function BookingCreateDialog() {
defaultValues: {
customerName: '',
customerPhone: '',
customerEmail: '',
},
});
@@ -154,6 +156,7 @@ export function BookingCreateDialog() {
startTime,
customerName: values.customerName,
customerPhone: values.customerPhone,
customerEmail: values.customerEmail || undefined,
});
};
@@ -271,6 +274,23 @@ export function BookingCreateDialog() {
<FieldError errors={[errors.customerPhone]} />
</Field>
<Field data-invalid={Boolean(errors.customerEmail)}>
<FieldLabel htmlFor="customerEmail">
Email <span className="text-muted-foreground font-normal">(opcional)</span>
</FieldLabel>
<Input
id="customerEmail"
type="email"
placeholder="Ej: juan@ejemplo.com"
aria-invalid={Boolean(errors.customerEmail)}
{...register('customerEmail')}
/>
<p className="mt-1 text-xs text-muted-foreground">
Se enviará la confirmación de la reserva por email.
</p>
<FieldError errors={[errors.customerEmail]} />
</Field>
{createBookingError && <p className="text-sm text-destructive">{createBookingError}</p>}
</form>

View File

@@ -62,6 +62,7 @@ const bookingFormSchema = z.object({
.trim()
.min(6, 'Ingresá un teléfono válido.')
.max(30, 'El teléfono no puede superar los 30 caracteres.'),
customerEmail: z.string().email('Ingresá un email válido.').optional().or(z.literal('')),
});
type BookingFormValues = z.infer<typeof bookingFormSchema>;
@@ -1294,6 +1295,24 @@ function SelectedSlotCard(props: BookingShellProps & { compact?: boolean }) {
</Field>
</div>
<Field data-invalid={Boolean(errors.customerEmail)}>
<FieldLabel htmlFor="customerEmail" className="text-white/76">
Email <span className="text-white/40 font-normal">(opcional)</span>
</FieldLabel>
<Input
id="customerEmail"
type="email"
placeholder="Ej: juan@ejemplo.com"
aria-invalid={Boolean(errors.customerEmail)}
className="border-white/10 bg-white/[0.045] text-white placeholder:text-white/32"
{...register('customerEmail')}
/>
<p className="mt-1 text-xs text-white/40">
Te enviaremos la confirmación de la reserva por email.
</p>
<FieldError errors={[errors.customerEmail]} />
</Field>
{createError && <p className="text-sm text-red-300">{createError}</p>}
{navigationError && <p className="text-sm text-red-300">{navigationError}</p>}
@@ -1409,6 +1428,7 @@ export function PublicBookingPage({ complexSlug }: PublicBookingPageProps) {
defaultValues: {
customerName: '',
customerPhone: '',
customerEmail: '',
},
});
@@ -1425,6 +1445,7 @@ export function PublicBookingPage({ complexSlug }: PublicBookingPageProps) {
startTime: selectedSlot.startTime,
customerName: payload.customerName,
customerPhone: payload.customerPhone,
customerEmail: payload.customerEmail || undefined,
});
},
});