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:
@@ -85,6 +85,7 @@ model CourtBooking {
|
|||||||
endTime String @map("end_time") @db.VarChar(5)
|
endTime String @map("end_time") @db.VarChar(5)
|
||||||
customerName String @map("customer_name") @db.VarChar(120)
|
customerName String @map("customer_name") @db.VarChar(120)
|
||||||
customerPhone String @map("customer_phone") @db.VarChar(30)
|
customerPhone String @map("customer_phone") @db.VarChar(30)
|
||||||
|
customerEmail String? @map("customer_email") @db.VarChar(254)
|
||||||
status CourtBookingStatus @default(CONFIRMED)
|
status CourtBookingStatus @default(CONFIRMED)
|
||||||
createdAt DateTime @default(now()) @map("created_at")
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
updatedAt DateTime @updatedAt @map("updated_at")
|
updatedAt DateTime @updatedAt @map("updated_at")
|
||||||
@@ -107,6 +108,7 @@ model CourtBookingLog {
|
|||||||
newStatus CourtBookingStatus @map("new_status")
|
newStatus CourtBookingStatus @map("new_status")
|
||||||
customerName String @map("customer_name") @db.VarChar(120)
|
customerName String @map("customer_name") @db.VarChar(120)
|
||||||
customerPhone String @map("customer_phone") @db.VarChar(30)
|
customerPhone String @map("customer_phone") @db.VarChar(30)
|
||||||
|
customerEmail String? @map("customer_email") @db.VarChar(254)
|
||||||
changedAt DateTime @default(now()) @map("changed_at")
|
changedAt DateTime @default(now()) @map("changed_at")
|
||||||
|
|
||||||
@@map("court_booking_logs")
|
@@map("court_booking_logs")
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the `onboarding_requests` table. If the table is not empty, all the data it contains will be lost.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "court_booking_logs" ADD COLUMN "customer_email" VARCHAR(254);
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "court_bookings" ADD COLUMN "customer_email" VARCHAR(254);
|
||||||
|
|
||||||
|
-- DropTable
|
||||||
|
DROP TABLE "onboarding_requests";
|
||||||
288
apps/backend/src/emails/booking-confirmation.ts
Normal file
288
apps/backend/src/emails/booking-confirmation.ts
Normal file
@@ -0,0 +1,288 @@
|
|||||||
|
type BookingEmailData = {
|
||||||
|
bookingCode: string;
|
||||||
|
complexName: string;
|
||||||
|
date: string;
|
||||||
|
startTime: string;
|
||||||
|
endTime: string;
|
||||||
|
courtName: string;
|
||||||
|
sportName: string;
|
||||||
|
customerName: string;
|
||||||
|
price?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
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: #f4f4f5;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||||
|
}
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
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:#f4f4f5;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:#f4f4f5;min-height:100vh;">
|
||||||
|
<tr>
|
||||||
|
<td align="center" style="padding:32px 16px;">
|
||||||
|
<table role="presentation" width="100%" style="max-width:520px;background-color:#ffffff;border-radius:12px;overflow:hidden;box-shadow:0 1px 3px rgba(0,0,0,0.08);">
|
||||||
|
${content}
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function headerSection() {
|
||||||
|
return `
|
||||||
|
<tr>
|
||||||
|
<td style="background-color:#0a1628;padding:32px 32px 28px;text-align:center;">
|
||||||
|
<span style="font-size:24px;font-weight:700;color:#10b981;letter-spacing:-0.3px;">Playzer</span>
|
||||||
|
</td>
|
||||||
|
</tr>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function divider() {
|
||||||
|
return `
|
||||||
|
<tr>
|
||||||
|
<td style="padding:0 32px;">
|
||||||
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0">
|
||||||
|
<tr>
|
||||||
|
<td style="height:1px;background-color:#e5e7eb;line-height:1px;font-size:1px;"> </td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function dateTimeBlock(date: string, startTime: string, endTime: string): string {
|
||||||
|
const friendlyDate = formatFriendlyDate(date);
|
||||||
|
|
||||||
|
return `
|
||||||
|
<tr>
|
||||||
|
<td style="padding:0 32px;">
|
||||||
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color:#f0fdf4;border-radius:10px;border:1px solid #bbf7d0;">
|
||||||
|
<tr>
|
||||||
|
<td style="padding:20px 24px;text-align:center;">
|
||||||
|
<p style="margin:0;font-size:16px;font-weight:700;color:#065f46;line-height:1.4;">
|
||||||
|
${friendlyDate}
|
||||||
|
</p>
|
||||||
|
<p style="margin:8px 0 0;font-size:22px;font-weight:800;color:#059669;letter-spacing:-0.3px;">
|
||||||
|
${startTime} — ${endTime}
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function bookingConfirmationHtml(data: BookingEmailData): string {
|
||||||
|
const formattedPrice =
|
||||||
|
data.price !== undefined && data.price > 0
|
||||||
|
? new Intl.NumberFormat('es-AR', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'ARS',
|
||||||
|
maximumFractionDigits: Number.isInteger(data.price) ? 0 : 2,
|
||||||
|
}).format(data.price)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const content = `
|
||||||
|
${headerSection()}
|
||||||
|
<tr>
|
||||||
|
<td style="padding:32px 32px 8px;">
|
||||||
|
<h1 style="margin:0;font-size:22px;font-weight:700;color:#111827;text-align:center;">
|
||||||
|
Reserva confirmada
|
||||||
|
</h1>
|
||||||
|
<p style="margin:8px 0 0;font-size:14px;color:#6b7280;text-align:center;">
|
||||||
|
${data.complexName}
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td style="padding:20px 32px 12px;">
|
||||||
|
${dateTimeBlock(data.date, data.startTime, data.endTime)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
${divider()}
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td style="padding:24px 32px;">
|
||||||
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0">
|
||||||
|
${detailRow('Código', `<span style="font-family:monospace;font-weight:700;letter-spacing:2px;">${data.bookingCode}</span>`)}
|
||||||
|
${detailRow('Cancha', `${data.courtName} — ${data.sportName}`)}
|
||||||
|
${detailRow('Cliente', data.customerName)}
|
||||||
|
${formattedPrice ? detailRow('Precio', formattedPrice) : ''}
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
${divider()}
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td style="padding:24px 32px;">
|
||||||
|
<p style="margin:0;font-size:13px;color:#6b7280;text-align:center;line-height:1.5;">
|
||||||
|
Presentá el código de reserva al llegar al complejo.
|
||||||
|
<br />
|
||||||
|
Si necesitás cancelar o modificar, contactate directamente con el complejo.
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td style="background-color:#f9fafb;padding:20px 32px;text-align:center;">
|
||||||
|
<p style="margin:0;font-size:12px;color:#9ca3af;">
|
||||||
|
Playzer — Reserva de canchas online
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>`;
|
||||||
|
|
||||||
|
return wrapLayout(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function bookingCancelledHtml(data: BookingEmailData): string {
|
||||||
|
const content = `
|
||||||
|
${headerSection()}
|
||||||
|
<tr>
|
||||||
|
<td style="padding:32px 32px 8px;">
|
||||||
|
<h1 style="margin:0;font-size:22px;font-weight:700;color:#dc2626;text-align:center;">
|
||||||
|
Reserva cancelada
|
||||||
|
</h1>
|
||||||
|
<p style="margin:8px 0 0;font-size:14px;color:#6b7280;text-align:center;">
|
||||||
|
${data.complexName}
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td style="padding:20px 32px 12px;">
|
||||||
|
${dateTimeBlock(data.date, data.startTime, data.endTime)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
${divider()}
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td style="padding:24px 32px;">
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td style="padding:0 32px 24px;">
|
||||||
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0">
|
||||||
|
${detailRow('Cancha', `${data.courtName} — ${data.sportName}`)}
|
||||||
|
${detailRow('Cliente', data.customerName)}
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td style="background-color:#f9fafb;padding:20px 32px;text-align:center;">
|
||||||
|
<p style="margin:0;font-size:12px;color:#9ca3af;">
|
||||||
|
Playzer — Reserva de canchas online
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>`;
|
||||||
|
|
||||||
|
return wrapLayout(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function bookingNoShowHtml(data: BookingEmailData): string {
|
||||||
|
const content = `
|
||||||
|
${headerSection()}
|
||||||
|
<tr>
|
||||||
|
<td style="padding:32px 32px 8px;">
|
||||||
|
<h1 style="margin:0;font-size:22px;font-weight:700;color:#d97706;text-align:center;">
|
||||||
|
Reserva no concretada
|
||||||
|
</h1>
|
||||||
|
<p style="margin:8px 0 0;font-size:14px;color:#6b7280;text-align:center;">
|
||||||
|
${data.complexName}
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td style="padding:20px 32px 12px;">
|
||||||
|
${dateTimeBlock(data.date, data.startTime, data.endTime)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
${divider()}
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td style="padding:24px 32px;">
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td style="padding:0 32px 24px;">
|
||||||
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0">
|
||||||
|
${detailRow('Cancha', `${data.courtName} — ${data.sportName}`)}
|
||||||
|
${detailRow('Cliente', data.customerName)}
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td style="background-color:#f9fafb;padding:20px 32px;text-align:center;">
|
||||||
|
<p style="margin:0;font-size:12px;color:#9ca3af;">
|
||||||
|
Playzer — Reserva de canchas online
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>`;
|
||||||
|
|
||||||
|
return wrapLayout(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
function detailRow(label: string, value: string): string {
|
||||||
|
return `
|
||||||
|
<tr>
|
||||||
|
<td style="padding:6px 0;">
|
||||||
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0">
|
||||||
|
<tr>
|
||||||
|
<td style="width:100px;font-size:13px;color:#6b7280;vertical-align:top;padding:2px 0;">
|
||||||
|
${label}
|
||||||
|
</td>
|
||||||
|
<td style="font-size:13px;font-weight:600;color:#111827;vertical-align:top;padding:2px 0;">
|
||||||
|
${value}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>`;
|
||||||
|
}
|
||||||
@@ -82,11 +82,6 @@ export type CourtBooking = Prisma.CourtBookingModel
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export type CourtBookingLog = Prisma.CourtBookingLogModel
|
export type CourtBookingLog = Prisma.CourtBookingLogModel
|
||||||
/**
|
|
||||||
* Model OnboardingRequest
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
export type OnboardingRequest = Prisma.OnboardingRequestModel
|
|
||||||
/**
|
/**
|
||||||
* Model PasswordResetRequest
|
* Model PasswordResetRequest
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -106,11 +106,6 @@ export type CourtBooking = Prisma.CourtBookingModel
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export type CourtBookingLog = Prisma.CourtBookingLogModel
|
export type CourtBookingLog = Prisma.CourtBookingLogModel
|
||||||
/**
|
|
||||||
* Model OnboardingRequest
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
export type OnboardingRequest = Prisma.OnboardingRequestModel
|
|
||||||
/**
|
/**
|
||||||
* Model PasswordResetRequest
|
* Model PasswordResetRequest
|
||||||
*
|
*
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -397,7 +397,6 @@ export const ModelName = {
|
|||||||
CourtPriceRule: 'CourtPriceRule',
|
CourtPriceRule: 'CourtPriceRule',
|
||||||
CourtBooking: 'CourtBooking',
|
CourtBooking: 'CourtBooking',
|
||||||
CourtBookingLog: 'CourtBookingLog',
|
CourtBookingLog: 'CourtBookingLog',
|
||||||
OnboardingRequest: 'OnboardingRequest',
|
|
||||||
PasswordResetRequest: 'PasswordResetRequest',
|
PasswordResetRequest: 'PasswordResetRequest',
|
||||||
Plan: 'Plan'
|
Plan: 'Plan'
|
||||||
} as const
|
} as const
|
||||||
@@ -415,7 +414,7 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
|
|||||||
omit: GlobalOmitOptions
|
omit: GlobalOmitOptions
|
||||||
}
|
}
|
||||||
meta: {
|
meta: {
|
||||||
modelProps: "user" | "session" | "account" | "verification" | "complex" | "complexUser" | "complexInvitation" | "sport" | "court" | "courtAvailability" | "courtPriceRule" | "courtBooking" | "courtBookingLog" | "onboardingRequest" | "passwordResetRequest" | "plan"
|
modelProps: "user" | "session" | "account" | "verification" | "complex" | "complexUser" | "complexInvitation" | "sport" | "court" | "courtAvailability" | "courtPriceRule" | "courtBooking" | "courtBookingLog" | "passwordResetRequest" | "plan"
|
||||||
txIsolationLevel: TransactionIsolationLevel
|
txIsolationLevel: TransactionIsolationLevel
|
||||||
}
|
}
|
||||||
model: {
|
model: {
|
||||||
@@ -1381,80 +1380,6 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
OnboardingRequest: {
|
|
||||||
payload: Prisma.$OnboardingRequestPayload<ExtArgs>
|
|
||||||
fields: Prisma.OnboardingRequestFieldRefs
|
|
||||||
operations: {
|
|
||||||
findUnique: {
|
|
||||||
args: Prisma.OnboardingRequestFindUniqueArgs<ExtArgs>
|
|
||||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$OnboardingRequestPayload> | null
|
|
||||||
}
|
|
||||||
findUniqueOrThrow: {
|
|
||||||
args: Prisma.OnboardingRequestFindUniqueOrThrowArgs<ExtArgs>
|
|
||||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$OnboardingRequestPayload>
|
|
||||||
}
|
|
||||||
findFirst: {
|
|
||||||
args: Prisma.OnboardingRequestFindFirstArgs<ExtArgs>
|
|
||||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$OnboardingRequestPayload> | null
|
|
||||||
}
|
|
||||||
findFirstOrThrow: {
|
|
||||||
args: Prisma.OnboardingRequestFindFirstOrThrowArgs<ExtArgs>
|
|
||||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$OnboardingRequestPayload>
|
|
||||||
}
|
|
||||||
findMany: {
|
|
||||||
args: Prisma.OnboardingRequestFindManyArgs<ExtArgs>
|
|
||||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$OnboardingRequestPayload>[]
|
|
||||||
}
|
|
||||||
create: {
|
|
||||||
args: Prisma.OnboardingRequestCreateArgs<ExtArgs>
|
|
||||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$OnboardingRequestPayload>
|
|
||||||
}
|
|
||||||
createMany: {
|
|
||||||
args: Prisma.OnboardingRequestCreateManyArgs<ExtArgs>
|
|
||||||
result: BatchPayload
|
|
||||||
}
|
|
||||||
createManyAndReturn: {
|
|
||||||
args: Prisma.OnboardingRequestCreateManyAndReturnArgs<ExtArgs>
|
|
||||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$OnboardingRequestPayload>[]
|
|
||||||
}
|
|
||||||
delete: {
|
|
||||||
args: Prisma.OnboardingRequestDeleteArgs<ExtArgs>
|
|
||||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$OnboardingRequestPayload>
|
|
||||||
}
|
|
||||||
update: {
|
|
||||||
args: Prisma.OnboardingRequestUpdateArgs<ExtArgs>
|
|
||||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$OnboardingRequestPayload>
|
|
||||||
}
|
|
||||||
deleteMany: {
|
|
||||||
args: Prisma.OnboardingRequestDeleteManyArgs<ExtArgs>
|
|
||||||
result: BatchPayload
|
|
||||||
}
|
|
||||||
updateMany: {
|
|
||||||
args: Prisma.OnboardingRequestUpdateManyArgs<ExtArgs>
|
|
||||||
result: BatchPayload
|
|
||||||
}
|
|
||||||
updateManyAndReturn: {
|
|
||||||
args: Prisma.OnboardingRequestUpdateManyAndReturnArgs<ExtArgs>
|
|
||||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$OnboardingRequestPayload>[]
|
|
||||||
}
|
|
||||||
upsert: {
|
|
||||||
args: Prisma.OnboardingRequestUpsertArgs<ExtArgs>
|
|
||||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$OnboardingRequestPayload>
|
|
||||||
}
|
|
||||||
aggregate: {
|
|
||||||
args: Prisma.OnboardingRequestAggregateArgs<ExtArgs>
|
|
||||||
result: runtime.Types.Utils.Optional<Prisma.AggregateOnboardingRequest>
|
|
||||||
}
|
|
||||||
groupBy: {
|
|
||||||
args: Prisma.OnboardingRequestGroupByArgs<ExtArgs>
|
|
||||||
result: runtime.Types.Utils.Optional<Prisma.OnboardingRequestGroupByOutputType>[]
|
|
||||||
}
|
|
||||||
count: {
|
|
||||||
args: Prisma.OnboardingRequestCountArgs<ExtArgs>
|
|
||||||
result: runtime.Types.Utils.Optional<Prisma.OnboardingRequestCountAggregateOutputType> | number
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
PasswordResetRequest: {
|
PasswordResetRequest: {
|
||||||
payload: Prisma.$PasswordResetRequestPayload<ExtArgs>
|
payload: Prisma.$PasswordResetRequestPayload<ExtArgs>
|
||||||
fields: Prisma.PasswordResetRequestFieldRefs
|
fields: Prisma.PasswordResetRequestFieldRefs
|
||||||
@@ -1806,6 +1731,7 @@ export const CourtBookingScalarFieldEnum = {
|
|||||||
endTime: 'endTime',
|
endTime: 'endTime',
|
||||||
customerName: 'customerName',
|
customerName: 'customerName',
|
||||||
customerPhone: 'customerPhone',
|
customerPhone: 'customerPhone',
|
||||||
|
customerEmail: 'customerEmail',
|
||||||
status: 'status',
|
status: 'status',
|
||||||
createdAt: 'createdAt',
|
createdAt: 'createdAt',
|
||||||
updatedAt: 'updatedAt'
|
updatedAt: 'updatedAt'
|
||||||
@@ -1825,30 +1751,13 @@ export const CourtBookingLogScalarFieldEnum = {
|
|||||||
newStatus: 'newStatus',
|
newStatus: 'newStatus',
|
||||||
customerName: 'customerName',
|
customerName: 'customerName',
|
||||||
customerPhone: 'customerPhone',
|
customerPhone: 'customerPhone',
|
||||||
|
customerEmail: 'customerEmail',
|
||||||
changedAt: 'changedAt'
|
changedAt: 'changedAt'
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export type CourtBookingLogScalarFieldEnum = (typeof CourtBookingLogScalarFieldEnum)[keyof typeof CourtBookingLogScalarFieldEnum]
|
export type CourtBookingLogScalarFieldEnum = (typeof CourtBookingLogScalarFieldEnum)[keyof typeof CourtBookingLogScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
export const OnboardingRequestScalarFieldEnum = {
|
|
||||||
id: 'id',
|
|
||||||
fullName: 'fullName',
|
|
||||||
email: 'email',
|
|
||||||
otpHash: 'otpHash',
|
|
||||||
otpExpiresAt: 'otpExpiresAt',
|
|
||||||
otpAttempts: 'otpAttempts',
|
|
||||||
otpLastSentAt: 'otpLastSentAt',
|
|
||||||
otpResendCount: 'otpResendCount',
|
|
||||||
emailVerifiedAt: 'emailVerifiedAt',
|
|
||||||
completedAt: 'completedAt',
|
|
||||||
createdAt: 'createdAt',
|
|
||||||
updatedAt: 'updatedAt'
|
|
||||||
} as const
|
|
||||||
|
|
||||||
export type OnboardingRequestScalarFieldEnum = (typeof OnboardingRequestScalarFieldEnum)[keyof typeof OnboardingRequestScalarFieldEnum]
|
|
||||||
|
|
||||||
|
|
||||||
export const PasswordResetRequestScalarFieldEnum = {
|
export const PasswordResetRequestScalarFieldEnum = {
|
||||||
id: 'id',
|
id: 'id',
|
||||||
email: 'email',
|
email: 'email',
|
||||||
@@ -2162,7 +2071,6 @@ export type GlobalOmitConfig = {
|
|||||||
courtPriceRule?: Prisma.CourtPriceRuleOmit
|
courtPriceRule?: Prisma.CourtPriceRuleOmit
|
||||||
courtBooking?: Prisma.CourtBookingOmit
|
courtBooking?: Prisma.CourtBookingOmit
|
||||||
courtBookingLog?: Prisma.CourtBookingLogOmit
|
courtBookingLog?: Prisma.CourtBookingLogOmit
|
||||||
onboardingRequest?: Prisma.OnboardingRequestOmit
|
|
||||||
passwordResetRequest?: Prisma.PasswordResetRequestOmit
|
passwordResetRequest?: Prisma.PasswordResetRequestOmit
|
||||||
plan?: Prisma.PlanOmit
|
plan?: Prisma.PlanOmit
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,7 +64,6 @@ export const ModelName = {
|
|||||||
CourtPriceRule: 'CourtPriceRule',
|
CourtPriceRule: 'CourtPriceRule',
|
||||||
CourtBooking: 'CourtBooking',
|
CourtBooking: 'CourtBooking',
|
||||||
CourtBookingLog: 'CourtBookingLog',
|
CourtBookingLog: 'CourtBookingLog',
|
||||||
OnboardingRequest: 'OnboardingRequest',
|
|
||||||
PasswordResetRequest: 'PasswordResetRequest',
|
PasswordResetRequest: 'PasswordResetRequest',
|
||||||
Plan: 'Plan'
|
Plan: 'Plan'
|
||||||
} as const
|
} as const
|
||||||
@@ -249,6 +248,7 @@ export const CourtBookingScalarFieldEnum = {
|
|||||||
endTime: 'endTime',
|
endTime: 'endTime',
|
||||||
customerName: 'customerName',
|
customerName: 'customerName',
|
||||||
customerPhone: 'customerPhone',
|
customerPhone: 'customerPhone',
|
||||||
|
customerEmail: 'customerEmail',
|
||||||
status: 'status',
|
status: 'status',
|
||||||
createdAt: 'createdAt',
|
createdAt: 'createdAt',
|
||||||
updatedAt: 'updatedAt'
|
updatedAt: 'updatedAt'
|
||||||
@@ -268,30 +268,13 @@ export const CourtBookingLogScalarFieldEnum = {
|
|||||||
newStatus: 'newStatus',
|
newStatus: 'newStatus',
|
||||||
customerName: 'customerName',
|
customerName: 'customerName',
|
||||||
customerPhone: 'customerPhone',
|
customerPhone: 'customerPhone',
|
||||||
|
customerEmail: 'customerEmail',
|
||||||
changedAt: 'changedAt'
|
changedAt: 'changedAt'
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export type CourtBookingLogScalarFieldEnum = (typeof CourtBookingLogScalarFieldEnum)[keyof typeof CourtBookingLogScalarFieldEnum]
|
export type CourtBookingLogScalarFieldEnum = (typeof CourtBookingLogScalarFieldEnum)[keyof typeof CourtBookingLogScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
export const OnboardingRequestScalarFieldEnum = {
|
|
||||||
id: 'id',
|
|
||||||
fullName: 'fullName',
|
|
||||||
email: 'email',
|
|
||||||
otpHash: 'otpHash',
|
|
||||||
otpExpiresAt: 'otpExpiresAt',
|
|
||||||
otpAttempts: 'otpAttempts',
|
|
||||||
otpLastSentAt: 'otpLastSentAt',
|
|
||||||
otpResendCount: 'otpResendCount',
|
|
||||||
emailVerifiedAt: 'emailVerifiedAt',
|
|
||||||
completedAt: 'completedAt',
|
|
||||||
createdAt: 'createdAt',
|
|
||||||
updatedAt: 'updatedAt'
|
|
||||||
} as const
|
|
||||||
|
|
||||||
export type OnboardingRequestScalarFieldEnum = (typeof OnboardingRequestScalarFieldEnum)[keyof typeof OnboardingRequestScalarFieldEnum]
|
|
||||||
|
|
||||||
|
|
||||||
export const PasswordResetRequestScalarFieldEnum = {
|
export const PasswordResetRequestScalarFieldEnum = {
|
||||||
id: 'id',
|
id: 'id',
|
||||||
email: 'email',
|
email: 'email',
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ export type * from './models/CourtAvailability'
|
|||||||
export type * from './models/CourtPriceRule'
|
export type * from './models/CourtPriceRule'
|
||||||
export type * from './models/CourtBooking'
|
export type * from './models/CourtBooking'
|
||||||
export type * from './models/CourtBookingLog'
|
export type * from './models/CourtBookingLog'
|
||||||
export type * from './models/OnboardingRequest'
|
|
||||||
export type * from './models/PasswordResetRequest'
|
export type * from './models/PasswordResetRequest'
|
||||||
export type * from './models/Plan'
|
export type * from './models/Plan'
|
||||||
export type * from './commonInputTypes'
|
export type * from './commonInputTypes'
|
||||||
@@ -33,6 +33,7 @@ export type CourtBookingMinAggregateOutputType = {
|
|||||||
endTime: string | null
|
endTime: string | null
|
||||||
customerName: string | null
|
customerName: string | null
|
||||||
customerPhone: string | null
|
customerPhone: string | null
|
||||||
|
customerEmail: string | null
|
||||||
status: $Enums.CourtBookingStatus | null
|
status: $Enums.CourtBookingStatus | null
|
||||||
createdAt: Date | null
|
createdAt: Date | null
|
||||||
updatedAt: Date | null
|
updatedAt: Date | null
|
||||||
@@ -47,6 +48,7 @@ export type CourtBookingMaxAggregateOutputType = {
|
|||||||
endTime: string | null
|
endTime: string | null
|
||||||
customerName: string | null
|
customerName: string | null
|
||||||
customerPhone: string | null
|
customerPhone: string | null
|
||||||
|
customerEmail: string | null
|
||||||
status: $Enums.CourtBookingStatus | null
|
status: $Enums.CourtBookingStatus | null
|
||||||
createdAt: Date | null
|
createdAt: Date | null
|
||||||
updatedAt: Date | null
|
updatedAt: Date | null
|
||||||
@@ -61,6 +63,7 @@ export type CourtBookingCountAggregateOutputType = {
|
|||||||
endTime: number
|
endTime: number
|
||||||
customerName: number
|
customerName: number
|
||||||
customerPhone: number
|
customerPhone: number
|
||||||
|
customerEmail: number
|
||||||
status: number
|
status: number
|
||||||
createdAt: number
|
createdAt: number
|
||||||
updatedAt: number
|
updatedAt: number
|
||||||
@@ -77,6 +80,7 @@ export type CourtBookingMinAggregateInputType = {
|
|||||||
endTime?: true
|
endTime?: true
|
||||||
customerName?: true
|
customerName?: true
|
||||||
customerPhone?: true
|
customerPhone?: true
|
||||||
|
customerEmail?: true
|
||||||
status?: true
|
status?: true
|
||||||
createdAt?: true
|
createdAt?: true
|
||||||
updatedAt?: true
|
updatedAt?: true
|
||||||
@@ -91,6 +95,7 @@ export type CourtBookingMaxAggregateInputType = {
|
|||||||
endTime?: true
|
endTime?: true
|
||||||
customerName?: true
|
customerName?: true
|
||||||
customerPhone?: true
|
customerPhone?: true
|
||||||
|
customerEmail?: true
|
||||||
status?: true
|
status?: true
|
||||||
createdAt?: true
|
createdAt?: true
|
||||||
updatedAt?: true
|
updatedAt?: true
|
||||||
@@ -105,6 +110,7 @@ export type CourtBookingCountAggregateInputType = {
|
|||||||
endTime?: true
|
endTime?: true
|
||||||
customerName?: true
|
customerName?: true
|
||||||
customerPhone?: true
|
customerPhone?: true
|
||||||
|
customerEmail?: true
|
||||||
status?: true
|
status?: true
|
||||||
createdAt?: true
|
createdAt?: true
|
||||||
updatedAt?: true
|
updatedAt?: true
|
||||||
@@ -192,6 +198,7 @@ export type CourtBookingGroupByOutputType = {
|
|||||||
endTime: string
|
endTime: string
|
||||||
customerName: string
|
customerName: string
|
||||||
customerPhone: string
|
customerPhone: string
|
||||||
|
customerEmail: string | null
|
||||||
status: $Enums.CourtBookingStatus
|
status: $Enums.CourtBookingStatus
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
updatedAt: Date
|
updatedAt: Date
|
||||||
@@ -227,6 +234,7 @@ export type CourtBookingWhereInput = {
|
|||||||
endTime?: Prisma.StringFilter<"CourtBooking"> | string
|
endTime?: Prisma.StringFilter<"CourtBooking"> | string
|
||||||
customerName?: Prisma.StringFilter<"CourtBooking"> | string
|
customerName?: Prisma.StringFilter<"CourtBooking"> | string
|
||||||
customerPhone?: Prisma.StringFilter<"CourtBooking"> | string
|
customerPhone?: Prisma.StringFilter<"CourtBooking"> | string
|
||||||
|
customerEmail?: Prisma.StringNullableFilter<"CourtBooking"> | string | null
|
||||||
status?: Prisma.EnumCourtBookingStatusFilter<"CourtBooking"> | $Enums.CourtBookingStatus
|
status?: Prisma.EnumCourtBookingStatusFilter<"CourtBooking"> | $Enums.CourtBookingStatus
|
||||||
createdAt?: Prisma.DateTimeFilter<"CourtBooking"> | Date | string
|
createdAt?: Prisma.DateTimeFilter<"CourtBooking"> | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFilter<"CourtBooking"> | Date | string
|
updatedAt?: Prisma.DateTimeFilter<"CourtBooking"> | Date | string
|
||||||
@@ -242,6 +250,7 @@ export type CourtBookingOrderByWithRelationInput = {
|
|||||||
endTime?: Prisma.SortOrder
|
endTime?: Prisma.SortOrder
|
||||||
customerName?: Prisma.SortOrder
|
customerName?: Prisma.SortOrder
|
||||||
customerPhone?: Prisma.SortOrder
|
customerPhone?: Prisma.SortOrder
|
||||||
|
customerEmail?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
status?: Prisma.SortOrder
|
status?: Prisma.SortOrder
|
||||||
createdAt?: Prisma.SortOrder
|
createdAt?: Prisma.SortOrder
|
||||||
updatedAt?: Prisma.SortOrder
|
updatedAt?: Prisma.SortOrder
|
||||||
@@ -261,6 +270,7 @@ export type CourtBookingWhereUniqueInput = Prisma.AtLeast<{
|
|||||||
endTime?: Prisma.StringFilter<"CourtBooking"> | string
|
endTime?: Prisma.StringFilter<"CourtBooking"> | string
|
||||||
customerName?: Prisma.StringFilter<"CourtBooking"> | string
|
customerName?: Prisma.StringFilter<"CourtBooking"> | string
|
||||||
customerPhone?: Prisma.StringFilter<"CourtBooking"> | string
|
customerPhone?: Prisma.StringFilter<"CourtBooking"> | string
|
||||||
|
customerEmail?: Prisma.StringNullableFilter<"CourtBooking"> | string | null
|
||||||
status?: Prisma.EnumCourtBookingStatusFilter<"CourtBooking"> | $Enums.CourtBookingStatus
|
status?: Prisma.EnumCourtBookingStatusFilter<"CourtBooking"> | $Enums.CourtBookingStatus
|
||||||
createdAt?: Prisma.DateTimeFilter<"CourtBooking"> | Date | string
|
createdAt?: Prisma.DateTimeFilter<"CourtBooking"> | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFilter<"CourtBooking"> | Date | string
|
updatedAt?: Prisma.DateTimeFilter<"CourtBooking"> | Date | string
|
||||||
@@ -276,6 +286,7 @@ export type CourtBookingOrderByWithAggregationInput = {
|
|||||||
endTime?: Prisma.SortOrder
|
endTime?: Prisma.SortOrder
|
||||||
customerName?: Prisma.SortOrder
|
customerName?: Prisma.SortOrder
|
||||||
customerPhone?: Prisma.SortOrder
|
customerPhone?: Prisma.SortOrder
|
||||||
|
customerEmail?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
status?: Prisma.SortOrder
|
status?: Prisma.SortOrder
|
||||||
createdAt?: Prisma.SortOrder
|
createdAt?: Prisma.SortOrder
|
||||||
updatedAt?: Prisma.SortOrder
|
updatedAt?: Prisma.SortOrder
|
||||||
@@ -296,6 +307,7 @@ export type CourtBookingScalarWhereWithAggregatesInput = {
|
|||||||
endTime?: Prisma.StringWithAggregatesFilter<"CourtBooking"> | string
|
endTime?: Prisma.StringWithAggregatesFilter<"CourtBooking"> | string
|
||||||
customerName?: Prisma.StringWithAggregatesFilter<"CourtBooking"> | string
|
customerName?: Prisma.StringWithAggregatesFilter<"CourtBooking"> | string
|
||||||
customerPhone?: Prisma.StringWithAggregatesFilter<"CourtBooking"> | string
|
customerPhone?: Prisma.StringWithAggregatesFilter<"CourtBooking"> | string
|
||||||
|
customerEmail?: Prisma.StringNullableWithAggregatesFilter<"CourtBooking"> | string | null
|
||||||
status?: Prisma.EnumCourtBookingStatusWithAggregatesFilter<"CourtBooking"> | $Enums.CourtBookingStatus
|
status?: Prisma.EnumCourtBookingStatusWithAggregatesFilter<"CourtBooking"> | $Enums.CourtBookingStatus
|
||||||
createdAt?: Prisma.DateTimeWithAggregatesFilter<"CourtBooking"> | Date | string
|
createdAt?: Prisma.DateTimeWithAggregatesFilter<"CourtBooking"> | Date | string
|
||||||
updatedAt?: Prisma.DateTimeWithAggregatesFilter<"CourtBooking"> | Date | string
|
updatedAt?: Prisma.DateTimeWithAggregatesFilter<"CourtBooking"> | Date | string
|
||||||
@@ -309,6 +321,7 @@ export type CourtBookingCreateInput = {
|
|||||||
endTime: string
|
endTime: string
|
||||||
customerName: string
|
customerName: string
|
||||||
customerPhone: string
|
customerPhone: string
|
||||||
|
customerEmail?: string | null
|
||||||
status?: $Enums.CourtBookingStatus
|
status?: $Enums.CourtBookingStatus
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
@@ -324,6 +337,7 @@ export type CourtBookingUncheckedCreateInput = {
|
|||||||
endTime: string
|
endTime: string
|
||||||
customerName: string
|
customerName: string
|
||||||
customerPhone: string
|
customerPhone: string
|
||||||
|
customerEmail?: string | null
|
||||||
status?: $Enums.CourtBookingStatus
|
status?: $Enums.CourtBookingStatus
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
@@ -337,6 +351,7 @@ export type CourtBookingUpdateInput = {
|
|||||||
endTime?: Prisma.StringFieldUpdateOperationsInput | string
|
endTime?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
customerName?: Prisma.StringFieldUpdateOperationsInput | string
|
customerName?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
customerPhone?: Prisma.StringFieldUpdateOperationsInput | string
|
customerPhone?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
customerEmail?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
status?: Prisma.EnumCourtBookingStatusFieldUpdateOperationsInput | $Enums.CourtBookingStatus
|
status?: Prisma.EnumCourtBookingStatusFieldUpdateOperationsInput | $Enums.CourtBookingStatus
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
@@ -352,6 +367,7 @@ export type CourtBookingUncheckedUpdateInput = {
|
|||||||
endTime?: Prisma.StringFieldUpdateOperationsInput | string
|
endTime?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
customerName?: Prisma.StringFieldUpdateOperationsInput | string
|
customerName?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
customerPhone?: Prisma.StringFieldUpdateOperationsInput | string
|
customerPhone?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
customerEmail?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
status?: Prisma.EnumCourtBookingStatusFieldUpdateOperationsInput | $Enums.CourtBookingStatus
|
status?: Prisma.EnumCourtBookingStatusFieldUpdateOperationsInput | $Enums.CourtBookingStatus
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
@@ -366,6 +382,7 @@ export type CourtBookingCreateManyInput = {
|
|||||||
endTime: string
|
endTime: string
|
||||||
customerName: string
|
customerName: string
|
||||||
customerPhone: string
|
customerPhone: string
|
||||||
|
customerEmail?: string | null
|
||||||
status?: $Enums.CourtBookingStatus
|
status?: $Enums.CourtBookingStatus
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
@@ -379,6 +396,7 @@ export type CourtBookingUpdateManyMutationInput = {
|
|||||||
endTime?: Prisma.StringFieldUpdateOperationsInput | string
|
endTime?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
customerName?: Prisma.StringFieldUpdateOperationsInput | string
|
customerName?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
customerPhone?: Prisma.StringFieldUpdateOperationsInput | string
|
customerPhone?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
customerEmail?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
status?: Prisma.EnumCourtBookingStatusFieldUpdateOperationsInput | $Enums.CourtBookingStatus
|
status?: Prisma.EnumCourtBookingStatusFieldUpdateOperationsInput | $Enums.CourtBookingStatus
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
@@ -393,6 +411,7 @@ export type CourtBookingUncheckedUpdateManyInput = {
|
|||||||
endTime?: Prisma.StringFieldUpdateOperationsInput | string
|
endTime?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
customerName?: Prisma.StringFieldUpdateOperationsInput | string
|
customerName?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
customerPhone?: Prisma.StringFieldUpdateOperationsInput | string
|
customerPhone?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
customerEmail?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
status?: Prisma.EnumCourtBookingStatusFieldUpdateOperationsInput | $Enums.CourtBookingStatus
|
status?: Prisma.EnumCourtBookingStatusFieldUpdateOperationsInput | $Enums.CourtBookingStatus
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
@@ -423,6 +442,7 @@ export type CourtBookingCountOrderByAggregateInput = {
|
|||||||
endTime?: Prisma.SortOrder
|
endTime?: Prisma.SortOrder
|
||||||
customerName?: Prisma.SortOrder
|
customerName?: Prisma.SortOrder
|
||||||
customerPhone?: Prisma.SortOrder
|
customerPhone?: Prisma.SortOrder
|
||||||
|
customerEmail?: Prisma.SortOrder
|
||||||
status?: Prisma.SortOrder
|
status?: Prisma.SortOrder
|
||||||
createdAt?: Prisma.SortOrder
|
createdAt?: Prisma.SortOrder
|
||||||
updatedAt?: Prisma.SortOrder
|
updatedAt?: Prisma.SortOrder
|
||||||
@@ -437,6 +457,7 @@ export type CourtBookingMaxOrderByAggregateInput = {
|
|||||||
endTime?: Prisma.SortOrder
|
endTime?: Prisma.SortOrder
|
||||||
customerName?: Prisma.SortOrder
|
customerName?: Prisma.SortOrder
|
||||||
customerPhone?: Prisma.SortOrder
|
customerPhone?: Prisma.SortOrder
|
||||||
|
customerEmail?: Prisma.SortOrder
|
||||||
status?: Prisma.SortOrder
|
status?: Prisma.SortOrder
|
||||||
createdAt?: Prisma.SortOrder
|
createdAt?: Prisma.SortOrder
|
||||||
updatedAt?: Prisma.SortOrder
|
updatedAt?: Prisma.SortOrder
|
||||||
@@ -451,6 +472,7 @@ export type CourtBookingMinOrderByAggregateInput = {
|
|||||||
endTime?: Prisma.SortOrder
|
endTime?: Prisma.SortOrder
|
||||||
customerName?: Prisma.SortOrder
|
customerName?: Prisma.SortOrder
|
||||||
customerPhone?: Prisma.SortOrder
|
customerPhone?: Prisma.SortOrder
|
||||||
|
customerEmail?: Prisma.SortOrder
|
||||||
status?: Prisma.SortOrder
|
status?: Prisma.SortOrder
|
||||||
createdAt?: Prisma.SortOrder
|
createdAt?: Prisma.SortOrder
|
||||||
updatedAt?: Prisma.SortOrder
|
updatedAt?: Prisma.SortOrder
|
||||||
@@ -510,6 +532,7 @@ export type CourtBookingCreateWithoutCourtInput = {
|
|||||||
endTime: string
|
endTime: string
|
||||||
customerName: string
|
customerName: string
|
||||||
customerPhone: string
|
customerPhone: string
|
||||||
|
customerEmail?: string | null
|
||||||
status?: $Enums.CourtBookingStatus
|
status?: $Enums.CourtBookingStatus
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
@@ -523,6 +546,7 @@ export type CourtBookingUncheckedCreateWithoutCourtInput = {
|
|||||||
endTime: string
|
endTime: string
|
||||||
customerName: string
|
customerName: string
|
||||||
customerPhone: string
|
customerPhone: string
|
||||||
|
customerEmail?: string | null
|
||||||
status?: $Enums.CourtBookingStatus
|
status?: $Enums.CourtBookingStatus
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
@@ -566,6 +590,7 @@ export type CourtBookingScalarWhereInput = {
|
|||||||
endTime?: Prisma.StringFilter<"CourtBooking"> | string
|
endTime?: Prisma.StringFilter<"CourtBooking"> | string
|
||||||
customerName?: Prisma.StringFilter<"CourtBooking"> | string
|
customerName?: Prisma.StringFilter<"CourtBooking"> | string
|
||||||
customerPhone?: Prisma.StringFilter<"CourtBooking"> | string
|
customerPhone?: Prisma.StringFilter<"CourtBooking"> | string
|
||||||
|
customerEmail?: Prisma.StringNullableFilter<"CourtBooking"> | string | null
|
||||||
status?: Prisma.EnumCourtBookingStatusFilter<"CourtBooking"> | $Enums.CourtBookingStatus
|
status?: Prisma.EnumCourtBookingStatusFilter<"CourtBooking"> | $Enums.CourtBookingStatus
|
||||||
createdAt?: Prisma.DateTimeFilter<"CourtBooking"> | Date | string
|
createdAt?: Prisma.DateTimeFilter<"CourtBooking"> | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFilter<"CourtBooking"> | Date | string
|
updatedAt?: Prisma.DateTimeFilter<"CourtBooking"> | Date | string
|
||||||
@@ -579,6 +604,7 @@ export type CourtBookingCreateManyCourtInput = {
|
|||||||
endTime: string
|
endTime: string
|
||||||
customerName: string
|
customerName: string
|
||||||
customerPhone: string
|
customerPhone: string
|
||||||
|
customerEmail?: string | null
|
||||||
status?: $Enums.CourtBookingStatus
|
status?: $Enums.CourtBookingStatus
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
@@ -592,6 +618,7 @@ export type CourtBookingUpdateWithoutCourtInput = {
|
|||||||
endTime?: Prisma.StringFieldUpdateOperationsInput | string
|
endTime?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
customerName?: Prisma.StringFieldUpdateOperationsInput | string
|
customerName?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
customerPhone?: Prisma.StringFieldUpdateOperationsInput | string
|
customerPhone?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
customerEmail?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
status?: Prisma.EnumCourtBookingStatusFieldUpdateOperationsInput | $Enums.CourtBookingStatus
|
status?: Prisma.EnumCourtBookingStatusFieldUpdateOperationsInput | $Enums.CourtBookingStatus
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
@@ -605,6 +632,7 @@ export type CourtBookingUncheckedUpdateWithoutCourtInput = {
|
|||||||
endTime?: Prisma.StringFieldUpdateOperationsInput | string
|
endTime?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
customerName?: Prisma.StringFieldUpdateOperationsInput | string
|
customerName?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
customerPhone?: Prisma.StringFieldUpdateOperationsInput | string
|
customerPhone?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
customerEmail?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
status?: Prisma.EnumCourtBookingStatusFieldUpdateOperationsInput | $Enums.CourtBookingStatus
|
status?: Prisma.EnumCourtBookingStatusFieldUpdateOperationsInput | $Enums.CourtBookingStatus
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
@@ -618,6 +646,7 @@ export type CourtBookingUncheckedUpdateManyWithoutCourtInput = {
|
|||||||
endTime?: Prisma.StringFieldUpdateOperationsInput | string
|
endTime?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
customerName?: Prisma.StringFieldUpdateOperationsInput | string
|
customerName?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
customerPhone?: Prisma.StringFieldUpdateOperationsInput | string
|
customerPhone?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
customerEmail?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
status?: Prisma.EnumCourtBookingStatusFieldUpdateOperationsInput | $Enums.CourtBookingStatus
|
status?: Prisma.EnumCourtBookingStatusFieldUpdateOperationsInput | $Enums.CourtBookingStatus
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
@@ -634,6 +663,7 @@ export type CourtBookingSelect<ExtArgs extends runtime.Types.Extensions.Internal
|
|||||||
endTime?: boolean
|
endTime?: boolean
|
||||||
customerName?: boolean
|
customerName?: boolean
|
||||||
customerPhone?: boolean
|
customerPhone?: boolean
|
||||||
|
customerEmail?: boolean
|
||||||
status?: boolean
|
status?: boolean
|
||||||
createdAt?: boolean
|
createdAt?: boolean
|
||||||
updatedAt?: boolean
|
updatedAt?: boolean
|
||||||
@@ -649,6 +679,7 @@ export type CourtBookingSelectCreateManyAndReturn<ExtArgs extends runtime.Types.
|
|||||||
endTime?: boolean
|
endTime?: boolean
|
||||||
customerName?: boolean
|
customerName?: boolean
|
||||||
customerPhone?: boolean
|
customerPhone?: boolean
|
||||||
|
customerEmail?: boolean
|
||||||
status?: boolean
|
status?: boolean
|
||||||
createdAt?: boolean
|
createdAt?: boolean
|
||||||
updatedAt?: boolean
|
updatedAt?: boolean
|
||||||
@@ -664,6 +695,7 @@ export type CourtBookingSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.
|
|||||||
endTime?: boolean
|
endTime?: boolean
|
||||||
customerName?: boolean
|
customerName?: boolean
|
||||||
customerPhone?: boolean
|
customerPhone?: boolean
|
||||||
|
customerEmail?: boolean
|
||||||
status?: boolean
|
status?: boolean
|
||||||
createdAt?: boolean
|
createdAt?: boolean
|
||||||
updatedAt?: boolean
|
updatedAt?: boolean
|
||||||
@@ -679,12 +711,13 @@ export type CourtBookingSelectScalar = {
|
|||||||
endTime?: boolean
|
endTime?: boolean
|
||||||
customerName?: boolean
|
customerName?: boolean
|
||||||
customerPhone?: boolean
|
customerPhone?: boolean
|
||||||
|
customerEmail?: boolean
|
||||||
status?: boolean
|
status?: boolean
|
||||||
createdAt?: boolean
|
createdAt?: boolean
|
||||||
updatedAt?: boolean
|
updatedAt?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CourtBookingOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "bookingCode" | "courtId" | "bookingDate" | "startTime" | "endTime" | "customerName" | "customerPhone" | "status" | "createdAt" | "updatedAt", ExtArgs["result"]["courtBooking"]>
|
export type CourtBookingOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "bookingCode" | "courtId" | "bookingDate" | "startTime" | "endTime" | "customerName" | "customerPhone" | "customerEmail" | "status" | "createdAt" | "updatedAt", ExtArgs["result"]["courtBooking"]>
|
||||||
export type CourtBookingInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
export type CourtBookingInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
court?: boolean | Prisma.CourtDefaultArgs<ExtArgs>
|
court?: boolean | Prisma.CourtDefaultArgs<ExtArgs>
|
||||||
}
|
}
|
||||||
@@ -709,6 +742,7 @@ export type $CourtBookingPayload<ExtArgs extends runtime.Types.Extensions.Intern
|
|||||||
endTime: string
|
endTime: string
|
||||||
customerName: string
|
customerName: string
|
||||||
customerPhone: string
|
customerPhone: string
|
||||||
|
customerEmail: string | null
|
||||||
status: $Enums.CourtBookingStatus
|
status: $Enums.CourtBookingStatus
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
updatedAt: Date
|
updatedAt: Date
|
||||||
@@ -1144,6 +1178,7 @@ export interface CourtBookingFieldRefs {
|
|||||||
readonly endTime: Prisma.FieldRef<"CourtBooking", 'String'>
|
readonly endTime: Prisma.FieldRef<"CourtBooking", 'String'>
|
||||||
readonly customerName: Prisma.FieldRef<"CourtBooking", 'String'>
|
readonly customerName: Prisma.FieldRef<"CourtBooking", 'String'>
|
||||||
readonly customerPhone: Prisma.FieldRef<"CourtBooking", 'String'>
|
readonly customerPhone: Prisma.FieldRef<"CourtBooking", 'String'>
|
||||||
|
readonly customerEmail: Prisma.FieldRef<"CourtBooking", 'String'>
|
||||||
readonly status: Prisma.FieldRef<"CourtBooking", 'CourtBookingStatus'>
|
readonly status: Prisma.FieldRef<"CourtBooking", 'CourtBookingStatus'>
|
||||||
readonly createdAt: Prisma.FieldRef<"CourtBooking", 'DateTime'>
|
readonly createdAt: Prisma.FieldRef<"CourtBooking", 'DateTime'>
|
||||||
readonly updatedAt: Prisma.FieldRef<"CourtBooking", 'DateTime'>
|
readonly updatedAt: Prisma.FieldRef<"CourtBooking", 'DateTime'>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,7 @@ import {
|
|||||||
AdminBookingServiceError,
|
AdminBookingServiceError,
|
||||||
createAdminBooking,
|
createAdminBooking,
|
||||||
} from '@/modules/admin-booking/services/admin-booking.service';
|
} from '@/modules/admin-booking/services/admin-booking.service';
|
||||||
|
import { sendBookingConfirmation } from '@/services/booking-email.service';
|
||||||
import type { AppContext } from '@/types/hono';
|
import type { AppContext } from '@/types/hono';
|
||||||
import type { CreateAdminBookingInput } from '@repo/api-contract';
|
import type { CreateAdminBookingInput } from '@repo/api-contract';
|
||||||
|
|
||||||
@@ -14,6 +15,20 @@ export async function createAdminBookingHandler(c: AppContext) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const booking = await createAdminBooking(user.id, complexId, payload);
|
const booking = await createAdminBooking(user.id, complexId, payload);
|
||||||
|
|
||||||
|
void sendBookingConfirmation({
|
||||||
|
bookingCode: booking.bookingCode,
|
||||||
|
complexName: booking.complexName,
|
||||||
|
date: booking.date,
|
||||||
|
startTime: booking.startTime,
|
||||||
|
endTime: booking.endTime,
|
||||||
|
courtName: booking.courtName,
|
||||||
|
sportName: booking.sport.name,
|
||||||
|
customerName: booking.customerName,
|
||||||
|
customerEmail: booking.customerEmail,
|
||||||
|
price: booking.price,
|
||||||
|
});
|
||||||
|
|
||||||
return c.json(booking, 201);
|
return c.json(booking, 201);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof AdminBookingServiceError) {
|
if (error instanceof AdminBookingServiceError) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import {
|
|||||||
AdminBookingServiceError,
|
AdminBookingServiceError,
|
||||||
updateAdminBookingStatus,
|
updateAdminBookingStatus,
|
||||||
} from '@/modules/admin-booking/services/admin-booking.service';
|
} from '@/modules/admin-booking/services/admin-booking.service';
|
||||||
|
import { sendBookingCancelled, sendBookingNoShow } from '@/services/booking-email.service';
|
||||||
import type { AppContext } from '@/types/hono';
|
import type { AppContext } from '@/types/hono';
|
||||||
import type { UpdateAdminBookingStatusInput } from '@repo/api-contract';
|
import type { UpdateAdminBookingStatusInput } from '@repo/api-contract';
|
||||||
|
|
||||||
@@ -14,6 +15,33 @@ export async function updateAdminBookingStatusHandler(c: AppContext) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const booking = await updateAdminBookingStatus(user.id, id, payload);
|
const booking = await updateAdminBookingStatus(user.id, id, payload);
|
||||||
|
|
||||||
|
if (payload.status === 'CANCELLED') {
|
||||||
|
void sendBookingCancelled({
|
||||||
|
bookingCode: booking.bookingCode,
|
||||||
|
complexName: booking.complexName,
|
||||||
|
date: booking.date,
|
||||||
|
startTime: booking.startTime,
|
||||||
|
endTime: booking.endTime,
|
||||||
|
courtName: booking.courtName,
|
||||||
|
sportName: booking.sport.name,
|
||||||
|
customerName: booking.customerName,
|
||||||
|
customerEmail: booking.customerEmail,
|
||||||
|
});
|
||||||
|
} else if (payload.status === 'NOSHOW') {
|
||||||
|
void sendBookingNoShow({
|
||||||
|
bookingCode: booking.bookingCode,
|
||||||
|
complexName: booking.complexName,
|
||||||
|
date: booking.date,
|
||||||
|
startTime: booking.startTime,
|
||||||
|
endTime: booking.endTime,
|
||||||
|
courtName: booking.courtName,
|
||||||
|
sportName: booking.sport.name,
|
||||||
|
customerName: booking.customerName,
|
||||||
|
customerEmail: booking.customerEmail,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return c.json(booking);
|
return c.json(booking);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof AdminBookingServiceError) {
|
if (error instanceof AdminBookingServiceError) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { randomInt } from 'node:crypto';
|
|||||||
import { CourtBookingStatus } from '@/generated/prisma/enums';
|
import { CourtBookingStatus } from '@/generated/prisma/enums';
|
||||||
import { db } from '@/lib/prisma';
|
import { db } from '@/lib/prisma';
|
||||||
import { evaluatePlanUsage, parsePlanRules } from '@/modules/plan/services/plan-rules.service';
|
import { evaluatePlanUsage, parsePlanRules } from '@/modules/plan/services/plan-rules.service';
|
||||||
|
import type { DayOfWeek } from '@repo/api-contract';
|
||||||
import type {
|
import type {
|
||||||
AdminBooking,
|
AdminBooking,
|
||||||
CreateAdminBookingInput,
|
CreateAdminBookingInput,
|
||||||
@@ -156,6 +157,40 @@ async function ensureComplexAccess(complexId: string, userId: string) {
|
|||||||
return complexUser.complex;
|
return complexUser.complex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolvePrice(
|
||||||
|
court: {
|
||||||
|
basePrice: unknown;
|
||||||
|
priceRules: Array<{
|
||||||
|
dayOfWeek: string | null;
|
||||||
|
startTime: string | null;
|
||||||
|
endTime: string | null;
|
||||||
|
price: unknown;
|
||||||
|
}>;
|
||||||
|
},
|
||||||
|
dayOfWeek: string,
|
||||||
|
startTime: string,
|
||||||
|
endTime: string
|
||||||
|
): number {
|
||||||
|
const slotStart = toMinutes(startTime);
|
||||||
|
const slotEnd = toMinutes(endTime);
|
||||||
|
|
||||||
|
const matchingRules = court.priceRules
|
||||||
|
.filter((rule) => {
|
||||||
|
if (rule.dayOfWeek && rule.dayOfWeek !== dayOfWeek) return false;
|
||||||
|
if (!rule.startTime || !rule.endTime) return true;
|
||||||
|
return slotStart >= toMinutes(rule.startTime) && slotEnd <= toMinutes(rule.endTime);
|
||||||
|
})
|
||||||
|
.sort((first, second) => {
|
||||||
|
const firstSpecificity =
|
||||||
|
(first.dayOfWeek ? 2 : 0) + (first.startTime && first.endTime ? 1 : 0);
|
||||||
|
const secondSpecificity =
|
||||||
|
(second.dayOfWeek ? 2 : 0) + (second.startTime && second.endTime ? 1 : 0);
|
||||||
|
return secondSpecificity - firstSpecificity;
|
||||||
|
});
|
||||||
|
|
||||||
|
return Number(matchingRules[0]?.price ?? court.basePrice);
|
||||||
|
}
|
||||||
|
|
||||||
function mapBookingResponse(booking: {
|
function mapBookingResponse(booking: {
|
||||||
id: string;
|
id: string;
|
||||||
bookingCode: string;
|
bookingCode: string;
|
||||||
@@ -164,6 +199,7 @@ function mapBookingResponse(booking: {
|
|||||||
endTime: string;
|
endTime: string;
|
||||||
customerName: string;
|
customerName: string;
|
||||||
customerPhone: string;
|
customerPhone: string;
|
||||||
|
customerEmail: string | null;
|
||||||
status: 'CONFIRMED' | 'CANCELLED' | 'COMPLETED' | 'NOSHOW';
|
status: 'CONFIRMED' | 'CANCELLED' | 'COMPLETED' | 'NOSHOW';
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
@@ -180,6 +216,7 @@ function mapBookingResponse(booking: {
|
|||||||
slug: string;
|
slug: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
price?: number;
|
||||||
}): AdminBooking {
|
}): AdminBooking {
|
||||||
return {
|
return {
|
||||||
id: booking.id,
|
id: booking.id,
|
||||||
@@ -198,6 +235,8 @@ function mapBookingResponse(booking: {
|
|||||||
endTime: booking.endTime,
|
endTime: booking.endTime,
|
||||||
customerName: booking.customerName,
|
customerName: booking.customerName,
|
||||||
customerPhone: booking.customerPhone,
|
customerPhone: booking.customerPhone,
|
||||||
|
customerEmail: booking.customerEmail ?? undefined,
|
||||||
|
price: booking.price ?? 0,
|
||||||
status: booking.status,
|
status: booking.status,
|
||||||
createdAt: booking.createdAt.toISOString(),
|
createdAt: booking.createdAt.toISOString(),
|
||||||
updatedAt: booking.updatedAt.toISOString(),
|
updatedAt: booking.updatedAt.toISOString(),
|
||||||
@@ -292,6 +331,10 @@ export async function createAdminBooking(
|
|||||||
slug: true,
|
slug: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
priceRules: {
|
||||||
|
where: { isActive: true },
|
||||||
|
orderBy: [{ dayOfWeek: 'asc' }, { startTime: 'asc' }],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -381,6 +424,7 @@ export async function createAdminBooking(
|
|||||||
endTime: selectedSlot.endTime,
|
endTime: selectedSlot.endTime,
|
||||||
customerName: input.customerName.trim(),
|
customerName: input.customerName.trim(),
|
||||||
customerPhone: input.customerPhone.trim(),
|
customerPhone: input.customerPhone.trim(),
|
||||||
|
customerEmail: input.customerEmail?.trim() || null,
|
||||||
status: 'CONFIRMED',
|
status: 'CONFIRMED',
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
@@ -407,7 +451,9 @@ export async function createAdminBooking(
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return mapBookingResponse(booking);
|
const price = resolvePrice(court, dayOfWeek, selectedSlot.startTime, selectedSlot.endTime);
|
||||||
|
|
||||||
|
return mapBookingResponse({ ...booking, price });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof AdminBookingServiceError) {
|
if (error instanceof AdminBookingServiceError) {
|
||||||
throw error;
|
throw error;
|
||||||
@@ -521,6 +567,7 @@ export async function updateAdminBookingStatus(
|
|||||||
endTime: booking.endTime,
|
endTime: booking.endTime,
|
||||||
customerName: booking.customerName,
|
customerName: booking.customerName,
|
||||||
customerPhone: booking.customerPhone,
|
customerPhone: booking.customerPhone,
|
||||||
|
customerEmail: booking.customerEmail,
|
||||||
previousStatus: booking.status,
|
previousStatus: booking.status,
|
||||||
newStatus: input.status,
|
newStatus: input.status,
|
||||||
changedAt: new Date(),
|
changedAt: new Date(),
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
PublicBookingServiceError,
|
PublicBookingServiceError,
|
||||||
createPublicBooking,
|
createPublicBooking,
|
||||||
} from '@/modules/public-booking/services/public-booking.service';
|
} from '@/modules/public-booking/services/public-booking.service';
|
||||||
|
import { sendBookingConfirmation } from '@/services/booking-email.service';
|
||||||
import type { AppContext } from '@/types/hono';
|
import type { AppContext } from '@/types/hono';
|
||||||
import type { CreatePublicBookingInput } from '@repo/api-contract';
|
import type { CreatePublicBookingInput } from '@repo/api-contract';
|
||||||
|
|
||||||
@@ -30,6 +31,19 @@ export async function createPublicBookingHandler(c: AppContext) {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
void sendBookingConfirmation({
|
||||||
|
bookingCode: booking.bookingCode,
|
||||||
|
complexName: booking.complexName,
|
||||||
|
date: booking.date,
|
||||||
|
startTime: booking.startTime,
|
||||||
|
endTime: booking.endTime,
|
||||||
|
courtName: booking.courtName,
|
||||||
|
sportName: booking.sport.name,
|
||||||
|
customerName: booking.customerName,
|
||||||
|
customerEmail: booking.customerEmail,
|
||||||
|
price: booking.price,
|
||||||
|
});
|
||||||
|
|
||||||
return c.json(booking, 201);
|
return c.json(booking, 201);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof PublicBookingServiceError) {
|
if (error instanceof PublicBookingServiceError) {
|
||||||
|
|||||||
@@ -276,6 +276,8 @@ function mapBookingResponse(input: {
|
|||||||
endTime: string;
|
endTime: string;
|
||||||
customerName: string;
|
customerName: string;
|
||||||
customerPhone: string;
|
customerPhone: string;
|
||||||
|
customerEmail: string | null;
|
||||||
|
price: number;
|
||||||
status: 'CONFIRMED' | 'CANCELLED' | 'COMPLETED' | 'NOSHOW';
|
status: 'CONFIRMED' | 'CANCELLED' | 'COMPLETED' | 'NOSHOW';
|
||||||
court: {
|
court: {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -304,7 +306,9 @@ function mapBookingResponse(input: {
|
|||||||
endTime: input.endTime,
|
endTime: input.endTime,
|
||||||
customerName: input.customerName,
|
customerName: input.customerName,
|
||||||
customerPhone: input.customerPhone,
|
customerPhone: input.customerPhone,
|
||||||
|
customerEmail: input.customerEmail ?? undefined,
|
||||||
status: input.status,
|
status: input.status,
|
||||||
|
price: input.price,
|
||||||
createdAt: input.createdAt.toISOString(),
|
createdAt: input.createdAt.toISOString(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -325,6 +329,7 @@ export async function getPublicBookingConfirmation(complexSlug: string, bookingC
|
|||||||
bookingDate: true,
|
bookingDate: true,
|
||||||
startTime: true,
|
startTime: true,
|
||||||
endTime: true,
|
endTime: true,
|
||||||
|
customerEmail: true,
|
||||||
status: true,
|
status: true,
|
||||||
createdAt: true,
|
createdAt: true,
|
||||||
court: {
|
court: {
|
||||||
@@ -381,6 +386,7 @@ export async function getPublicBookingConfirmation(complexSlug: string, bookingC
|
|||||||
slug: booking.court.sport.slug,
|
slug: booking.court.sport.slug,
|
||||||
},
|
},
|
||||||
status: booking.status,
|
status: booking.status,
|
||||||
|
customerEmail: booking.customerEmail ?? undefined,
|
||||||
createdAt: booking.createdAt.toISOString(),
|
createdAt: booking.createdAt.toISOString(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -625,6 +631,7 @@ export async function createPublicBooking(complexSlug: string, input: CreatePubl
|
|||||||
endTime: selectedSlot.endTime,
|
endTime: selectedSlot.endTime,
|
||||||
customerName: input.customerName.trim(),
|
customerName: input.customerName.trim(),
|
||||||
customerPhone: input.customerPhone.trim(),
|
customerPhone: input.customerPhone.trim(),
|
||||||
|
customerEmail: input.customerEmail?.trim() || null,
|
||||||
status: 'CONFIRMED',
|
status: 'CONFIRMED',
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
@@ -636,11 +643,14 @@ export async function createPublicBooking(complexSlug: string, input: CreatePubl
|
|||||||
endTime: true,
|
endTime: true,
|
||||||
customerName: true,
|
customerName: true,
|
||||||
customerPhone: true,
|
customerPhone: true,
|
||||||
|
customerEmail: true,
|
||||||
status: true,
|
status: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const price = resolveSlotPrice(selectedCourt, dayOfWeek, selectedSlot);
|
||||||
|
|
||||||
return mapBookingResponse({
|
return mapBookingResponse({
|
||||||
bookingId: booking.id,
|
bookingId: booking.id,
|
||||||
bookingCode: booking.bookingCode,
|
bookingCode: booking.bookingCode,
|
||||||
@@ -650,6 +660,8 @@ export async function createPublicBooking(complexSlug: string, input: CreatePubl
|
|||||||
endTime: booking.endTime,
|
endTime: booking.endTime,
|
||||||
customerName: booking.customerName,
|
customerName: booking.customerName,
|
||||||
customerPhone: booking.customerPhone,
|
customerPhone: booking.customerPhone,
|
||||||
|
customerEmail: booking.customerEmail,
|
||||||
|
price,
|
||||||
status: booking.status,
|
status: booking.status,
|
||||||
court: {
|
court: {
|
||||||
id: selectedCourt.id,
|
id: selectedCourt.id,
|
||||||
|
|||||||
86
apps/backend/src/services/booking-email.service.ts
Normal file
86
apps/backend/src/services/booking-email.service.ts
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import {
|
||||||
|
bookingCancelledHtml,
|
||||||
|
bookingConfirmationHtml,
|
||||||
|
bookingNoShowHtml,
|
||||||
|
} from '@/emails/booking-confirmation';
|
||||||
|
import { sendMail } from '@/lib/mailer';
|
||||||
|
|
||||||
|
type BookingEmailInput = {
|
||||||
|
bookingCode: string;
|
||||||
|
complexName: string;
|
||||||
|
date: string;
|
||||||
|
startTime: string;
|
||||||
|
endTime: string;
|
||||||
|
courtName: string;
|
||||||
|
sportName: string;
|
||||||
|
customerName: string;
|
||||||
|
customerEmail?: string;
|
||||||
|
price?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function sendBookingConfirmation(data: BookingEmailInput) {
|
||||||
|
if (!data.customerEmail) return;
|
||||||
|
|
||||||
|
const html = bookingConfirmationHtml({
|
||||||
|
bookingCode: data.bookingCode,
|
||||||
|
complexName: data.complexName,
|
||||||
|
date: data.date,
|
||||||
|
startTime: data.startTime,
|
||||||
|
endTime: data.endTime,
|
||||||
|
courtName: data.courtName,
|
||||||
|
sportName: data.sportName,
|
||||||
|
customerName: data.customerName,
|
||||||
|
price: data.price,
|
||||||
|
});
|
||||||
|
|
||||||
|
await sendMail({
|
||||||
|
to: data.customerEmail,
|
||||||
|
subject: `Reserva confirmada en ${data.complexName} | Playzer`,
|
||||||
|
html,
|
||||||
|
text: `Reserva confirmada en ${data.complexName}. Codigo: ${data.bookingCode}. Cancha: ${data.courtName}. Fecha: ${data.date}. Horario: ${data.startTime} - ${data.endTime}.`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function sendBookingCancelled(data: BookingEmailInput) {
|
||||||
|
if (!data.customerEmail) return;
|
||||||
|
|
||||||
|
const html = bookingCancelledHtml({
|
||||||
|
bookingCode: data.bookingCode,
|
||||||
|
complexName: data.complexName,
|
||||||
|
date: data.date,
|
||||||
|
startTime: data.startTime,
|
||||||
|
endTime: data.endTime,
|
||||||
|
courtName: data.courtName,
|
||||||
|
sportName: data.sportName,
|
||||||
|
customerName: data.customerName,
|
||||||
|
});
|
||||||
|
|
||||||
|
await sendMail({
|
||||||
|
to: data.customerEmail,
|
||||||
|
subject: `Reserva cancelada en ${data.complexName} | Playzer`,
|
||||||
|
html,
|
||||||
|
text: `Reserva cancelada en ${data.complexName}. Codigo: ${data.bookingCode}. Fecha: ${data.date}. Horario: ${data.startTime} - ${data.endTime}.`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function sendBookingNoShow(data: BookingEmailInput) {
|
||||||
|
if (!data.customerEmail) return;
|
||||||
|
|
||||||
|
const html = bookingNoShowHtml({
|
||||||
|
bookingCode: data.bookingCode,
|
||||||
|
complexName: data.complexName,
|
||||||
|
date: data.date,
|
||||||
|
startTime: data.startTime,
|
||||||
|
endTime: data.endTime,
|
||||||
|
courtName: data.courtName,
|
||||||
|
sportName: data.sportName,
|
||||||
|
customerName: data.customerName,
|
||||||
|
});
|
||||||
|
|
||||||
|
await sendMail({
|
||||||
|
to: data.customerEmail,
|
||||||
|
subject: `Reserva no concretada en ${data.complexName} | Playzer`,
|
||||||
|
html,
|
||||||
|
text: `Reserva no concretada en ${data.complexName}. Codigo: ${data.bookingCode}. Fecha: ${data.date}. Horario: ${data.startTime} - ${data.endTime}.`,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -41,6 +41,7 @@ interface CreateManualBookingPayload {
|
|||||||
startTime: string;
|
startTime: string;
|
||||||
customerName: string;
|
customerName: string;
|
||||||
customerPhone: string;
|
customerPhone: string;
|
||||||
|
customerEmail?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BookingContextValue {
|
interface BookingContextValue {
|
||||||
@@ -315,6 +316,7 @@ export function BookingProvider({ children, complex }: BookingProviderProps) {
|
|||||||
startTime: payload.startTime,
|
startTime: payload.startTime,
|
||||||
customerName: payload.customerName,
|
customerName: payload.customerName,
|
||||||
customerPhone: payload.customerPhone,
|
customerPhone: payload.customerPhone,
|
||||||
|
customerEmail: payload.customerEmail || undefined,
|
||||||
}),
|
}),
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
await queryClient.invalidateQueries({ queryKey: ['admin-bookings', complex.id] });
|
await queryClient.invalidateQueries({ queryKey: ['admin-bookings', complex.id] });
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ const bookingFormSchema = z.object({
|
|||||||
.trim()
|
.trim()
|
||||||
.min(6, 'Ingresa un telefono válido.')
|
.min(6, 'Ingresa un telefono válido.')
|
||||||
.max(30, 'El telefono no puede superar los 30 caracteres.'),
|
.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>;
|
type BookingForm = z.infer<typeof bookingFormSchema>;
|
||||||
@@ -76,6 +77,7 @@ export function BookingCreateDialog() {
|
|||||||
defaultValues: {
|
defaultValues: {
|
||||||
customerName: '',
|
customerName: '',
|
||||||
customerPhone: '',
|
customerPhone: '',
|
||||||
|
customerEmail: '',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -154,6 +156,7 @@ export function BookingCreateDialog() {
|
|||||||
startTime,
|
startTime,
|
||||||
customerName: values.customerName,
|
customerName: values.customerName,
|
||||||
customerPhone: values.customerPhone,
|
customerPhone: values.customerPhone,
|
||||||
|
customerEmail: values.customerEmail || undefined,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -271,6 +274,23 @@ export function BookingCreateDialog() {
|
|||||||
<FieldError errors={[errors.customerPhone]} />
|
<FieldError errors={[errors.customerPhone]} />
|
||||||
</Field>
|
</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>}
|
{createBookingError && <p className="text-sm text-destructive">{createBookingError}</p>}
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ const bookingFormSchema = z.object({
|
|||||||
.trim()
|
.trim()
|
||||||
.min(6, 'Ingresá un teléfono válido.')
|
.min(6, 'Ingresá un teléfono válido.')
|
||||||
.max(30, 'El teléfono no puede superar los 30 caracteres.'),
|
.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>;
|
type BookingFormValues = z.infer<typeof bookingFormSchema>;
|
||||||
@@ -1294,6 +1295,24 @@ function SelectedSlotCard(props: BookingShellProps & { compact?: boolean }) {
|
|||||||
</Field>
|
</Field>
|
||||||
</div>
|
</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>}
|
{createError && <p className="text-sm text-red-300">{createError}</p>}
|
||||||
{navigationError && <p className="text-sm text-red-300">{navigationError}</p>}
|
{navigationError && <p className="text-sm text-red-300">{navigationError}</p>}
|
||||||
|
|
||||||
@@ -1409,6 +1428,7 @@ export function PublicBookingPage({ complexSlug }: PublicBookingPageProps) {
|
|||||||
defaultValues: {
|
defaultValues: {
|
||||||
customerName: '',
|
customerName: '',
|
||||||
customerPhone: '',
|
customerPhone: '',
|
||||||
|
customerEmail: '',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1425,6 +1445,7 @@ export function PublicBookingPage({ complexSlug }: PublicBookingPageProps) {
|
|||||||
startTime: selectedSlot.startTime,
|
startTime: selectedSlot.startTime,
|
||||||
customerName: payload.customerName,
|
customerName: payload.customerName,
|
||||||
customerPhone: payload.customerPhone,
|
customerPhone: payload.customerPhone,
|
||||||
|
customerEmail: payload.customerEmail || undefined,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ export const adminBookingSchema = z.object({
|
|||||||
endTime: z.string().regex(TIME_REGEX),
|
endTime: z.string().regex(TIME_REGEX),
|
||||||
customerName: z.string(),
|
customerName: z.string(),
|
||||||
customerPhone: z.string(),
|
customerPhone: z.string(),
|
||||||
|
customerEmail: z.string().optional(),
|
||||||
|
price: z.number().nonnegative(),
|
||||||
status: bookingStatusSchema,
|
status: bookingStatusSchema,
|
||||||
createdAt: z.string().datetime(),
|
createdAt: z.string().datetime(),
|
||||||
updatedAt: z.string().datetime(),
|
updatedAt: z.string().datetime(),
|
||||||
@@ -52,6 +54,11 @@ export const createAdminBookingSchema = z.object({
|
|||||||
.trim()
|
.trim()
|
||||||
.min(6, 'El telefono debe tener al menos 6 caracteres.')
|
.min(6, 'El telefono debe tener al menos 6 caracteres.')
|
||||||
.max(30, 'El telefono no puede superar los 30 caracteres.'),
|
.max(30, 'El telefono no puede superar los 30 caracteres.'),
|
||||||
|
customerEmail: z
|
||||||
|
.string()
|
||||||
|
.email('El email ingresado no es valido.')
|
||||||
|
.optional()
|
||||||
|
.or(z.literal('')),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const updateAdminBookingStatusSchema = z.object({
|
export const updateAdminBookingStatusSchema = z.object({
|
||||||
|
|||||||
@@ -57,6 +57,11 @@ export const createPublicBookingSchema = z.object({
|
|||||||
.trim()
|
.trim()
|
||||||
.min(6, 'El telefono debe tener al menos 6 caracteres.')
|
.min(6, 'El telefono debe tener al menos 6 caracteres.')
|
||||||
.max(30, 'El telefono no puede superar los 30 caracteres.'),
|
.max(30, 'El telefono no puede superar los 30 caracteres.'),
|
||||||
|
customerEmail: z
|
||||||
|
.string()
|
||||||
|
.email('El email ingresado no es valido.')
|
||||||
|
.optional()
|
||||||
|
.or(z.literal('')),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const publicBookingSchema = z.object({
|
export const publicBookingSchema = z.object({
|
||||||
@@ -73,7 +78,9 @@ export const publicBookingSchema = z.object({
|
|||||||
endTime: z.string().regex(TIME_REGEX),
|
endTime: z.string().regex(TIME_REGEX),
|
||||||
customerName: z.string(),
|
customerName: z.string(),
|
||||||
customerPhone: z.string(),
|
customerPhone: z.string(),
|
||||||
|
customerEmail: z.string().optional(),
|
||||||
status: z.enum(['CONFIRMED', 'CANCELLED', 'COMPLETED']),
|
status: z.enum(['CONFIRMED', 'CANCELLED', 'COMPLETED']),
|
||||||
|
price: z.number().nonnegative(),
|
||||||
createdAt: z.string().datetime(),
|
createdAt: z.string().datetime(),
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -89,6 +96,7 @@ export const publicBookingConfirmationSchema = z.object({
|
|||||||
courtName: z.string(),
|
courtName: z.string(),
|
||||||
sport: publicBookingSportSchema,
|
sport: publicBookingSportSchema,
|
||||||
status: z.enum(['CONFIRMED', 'CANCELLED', 'COMPLETED']),
|
status: z.enum(['CONFIRMED', 'CANCELLED', 'COMPLETED']),
|
||||||
|
customerEmail: z.string().optional(),
|
||||||
createdAt: z.string().datetime(),
|
createdAt: z.string().datetime(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user