Compare commits
7 Commits
feat/admin
...
3949c9add1
| Author | SHA1 | Date | |
|---|---|---|---|
| 3949c9add1 | |||
|
|
dce312d426 | ||
| fd4d8b7abd | |||
|
|
3b3def94ab | ||
|
|
1318e3bf57 | ||
|
|
1b5eb253f2 | ||
| ccee416b6f |
56
AGENTS.md
56
AGENTS.md
@@ -33,6 +33,62 @@ packages/api-contract/ # Shared Zod schemas, types, route definitions
|
||||
2. Implement handler in `apps/backend`
|
||||
3. Consume from `apps/frontend` via workspace import `@repo/api-contract`
|
||||
|
||||
## Endpoint Pattern
|
||||
|
||||
Every new endpoint must follow the **Result pattern** with the **validate helper**.
|
||||
|
||||
### Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `apps/backend/src/lib/result.ts` | `Result<T>` type, `ok()`, `err()` |
|
||||
| `apps/backend/src/lib/errors.ts` | `AppError` discriminated union, `Errors` factory |
|
||||
| `apps/backend/src/lib/http/handle-result.ts` | `handleResult()` — bridges `Result` to HTTP |
|
||||
| `apps/backend/src/lib/http/validate.ts` | `validate.json()`, `.query()`, `.param()`, `.header()`, `.form()` |
|
||||
|
||||
### Layers
|
||||
|
||||
**1. Route** — Use `validate.json(schema)`, `validate.param(schema)`, etc. as Hono middleware.
|
||||
|
||||
```typescript
|
||||
import { validate } from '@/lib/http/validate';
|
||||
import { z } from 'zod';
|
||||
|
||||
const idParams = z.object({ id: z.string() });
|
||||
router.post('/', validate.json(createSchema), createHandler);
|
||||
router.get('/:id', validate.param(idParams), getHandler);
|
||||
```
|
||||
|
||||
**2. Service** — Return `Result<T>`. Use `ok(value)` on success, `err(Errors.*(...))` on failure.
|
||||
|
||||
```typescript
|
||||
import { Result, ok, err } from '@/lib/result';
|
||||
import { Errors } from '@/lib/errors';
|
||||
|
||||
export async function doSomething(input: Input): Promise<Result<Output>> {
|
||||
if (conflict) return err(Errors.conflict('Already exists'));
|
||||
return ok(result);
|
||||
}
|
||||
```
|
||||
|
||||
**3. Handler** — Call the service and pass the `Result` to `handleResult`.
|
||||
|
||||
```typescript
|
||||
import { handleResult } from '@/lib/http/handle-result';
|
||||
import { doSomething } from './service';
|
||||
|
||||
export async function myHandler(c: AppContext) {
|
||||
const payload = c.req.valid('json') as Input;
|
||||
return handleResult(c, await doSomething(payload), 201);
|
||||
}
|
||||
```
|
||||
|
||||
### Never
|
||||
|
||||
- ❌ Throw custom error classes from services
|
||||
- ❌ Use try/catch in handlers for business logic errors
|
||||
- ❌ Use `zValidator` directly — always use `validate.*`
|
||||
|
||||
## Authentication (Better Auth)
|
||||
|
||||
The project uses **Better Auth** for session management, replacing the legacy Supabase Auth.
|
||||
|
||||
@@ -14,6 +14,7 @@ model Complex {
|
||||
users ComplexUser[]
|
||||
invitations ComplexInvitation[]
|
||||
courts Court[]
|
||||
recurringGroups RecurringBookingGroup[]
|
||||
|
||||
@@index([planCode])
|
||||
@@index([complexSlug])
|
||||
|
||||
@@ -15,6 +15,11 @@ enum CourtBookingStatus {
|
||||
NOSHOW
|
||||
}
|
||||
|
||||
enum RecurringBookingGroupStatus {
|
||||
ACTIVE
|
||||
CANCELLED
|
||||
}
|
||||
|
||||
model Sport {
|
||||
id String @id @db.Uuid
|
||||
name String @unique
|
||||
@@ -44,6 +49,7 @@ model Court {
|
||||
priceRules CourtPriceRule[]
|
||||
bookings CourtBooking[]
|
||||
maintenances CourtMaintenance[]
|
||||
recurringGroups RecurringBookingGroup[]
|
||||
|
||||
@@index([complexId])
|
||||
@@index([sportId])
|
||||
@@ -95,40 +101,71 @@ model CourtPriceRule {
|
||||
}
|
||||
|
||||
model CourtBooking {
|
||||
id String @id @db.Uuid
|
||||
bookingCode String @unique @map("booking_code") @db.VarChar(8)
|
||||
courtId String @map("court_id") @db.Uuid
|
||||
bookingDate DateTime @map("booking_date") @db.Date
|
||||
startTime String @map("start_time") @db.VarChar(5)
|
||||
endTime String @map("end_time") @db.VarChar(5)
|
||||
customerName String @map("customer_name") @db.VarChar(120)
|
||||
customerPhone String @map("customer_phone") @db.VarChar(30)
|
||||
customerEmail String @map("customer_email") @db.VarChar(254)
|
||||
status CourtBookingStatus @default(CONFIRMED)
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
court Court @relation(fields: [courtId], references: [id], onDelete: Cascade)
|
||||
id String @id @db.Uuid
|
||||
bookingCode String @unique @map("booking_code") @db.VarChar(8)
|
||||
courtId String @map("court_id") @db.Uuid
|
||||
bookingDate DateTime @map("booking_date") @db.Date
|
||||
startTime String @map("start_time") @db.VarChar(5)
|
||||
endTime String @map("end_time") @db.VarChar(5)
|
||||
customerName String @map("customer_name") @db.VarChar(120)
|
||||
customerPhone String @map("customer_phone") @db.VarChar(30)
|
||||
customerEmail String @map("customer_email") @db.VarChar(254)
|
||||
status CourtBookingStatus @default(CONFIRMED)
|
||||
recurringGroupId String? @map("recurring_group_id") @db.Uuid
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
court Court @relation(fields: [courtId], references: [id], onDelete: Cascade)
|
||||
recurringGroup RecurringBookingGroup? @relation(fields: [recurringGroupId], references: [id])
|
||||
|
||||
@@unique([courtId, bookingDate, startTime])
|
||||
@@index([courtId, bookingDate])
|
||||
@@index([bookingDate])
|
||||
@@index([recurringGroupId])
|
||||
@@map("court_bookings")
|
||||
}
|
||||
|
||||
model CourtBookingLog {
|
||||
id String @id @db.Uuid
|
||||
bookingCode String @map("booking_code") @db.VarChar(8)
|
||||
courtId String @map("court_id") @db.Uuid
|
||||
bookingDate DateTime @map("booking_date") @db.Date
|
||||
startTime String @map("start_time") @db.VarChar(5)
|
||||
endTime String @map("end_time") @db.VarChar(5)
|
||||
previousStatus CourtBookingStatus @map("previous_status")
|
||||
newStatus CourtBookingStatus @map("new_status")
|
||||
customerName String @map("customer_name") @db.VarChar(120)
|
||||
customerPhone String @map("customer_phone") @db.VarChar(30)
|
||||
customerEmail String @map("customer_email") @db.VarChar(254)
|
||||
changedAt DateTime @default(now()) @map("changed_at")
|
||||
id String @id @db.Uuid
|
||||
bookingCode String @map("booking_code") @db.VarChar(8)
|
||||
courtId String @map("court_id") @db.Uuid
|
||||
bookingDate DateTime @map("booking_date") @db.Date
|
||||
startTime String @map("start_time") @db.VarChar(5)
|
||||
endTime String @map("end_time") @db.VarChar(5)
|
||||
previousStatus CourtBookingStatus @map("previous_status")
|
||||
newStatus CourtBookingStatus @map("new_status")
|
||||
previousCourtId String? @map("previous_court_id") @db.Uuid
|
||||
previousStartTime String? @map("previous_start_time") @db.VarChar(5)
|
||||
previousEndTime String? @map("previous_end_time") @db.VarChar(5)
|
||||
customerName String @map("customer_name") @db.VarChar(120)
|
||||
customerPhone String @map("customer_phone") @db.VarChar(30)
|
||||
customerEmail String @map("customer_email") @db.VarChar(254)
|
||||
changedAt DateTime @default(now()) @map("changed_at")
|
||||
|
||||
@@map("court_booking_logs")
|
||||
@@index([courtId, newStatus])
|
||||
}
|
||||
|
||||
model RecurringBookingGroup {
|
||||
id String @id @db.Uuid
|
||||
complexId String @map("complex_id") @db.Uuid
|
||||
courtId String @map("court_id") @db.Uuid
|
||||
startTime String @map("start_time") @db.VarChar(5)
|
||||
endTime String @map("end_time") @db.VarChar(5)
|
||||
dayOfWeek DayOfWeek @map("day_of_week")
|
||||
startDate DateTime @map("start_date") @db.Date
|
||||
endDate DateTime? @map("end_date") @db.Date
|
||||
status RecurringBookingGroupStatus @default(ACTIVE)
|
||||
customerName String @map("customer_name") @db.VarChar(120)
|
||||
customerPhone String @map("customer_phone") @db.VarChar(30)
|
||||
customerEmail String @map("customer_email") @db.VarChar(254)
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
court Court @relation(fields: [courtId], references: [id], onDelete: Cascade)
|
||||
complex Complex @relation(fields: [complexId], references: [id], onDelete: Cascade)
|
||||
bookings CourtBooking[]
|
||||
|
||||
@@index([complexId])
|
||||
@@index([courtId])
|
||||
@@index([status])
|
||||
@@map("recurring_booking_groups")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "RecurringBookingGroupStatus" AS ENUM ('ACTIVE', 'CANCELLED');
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "court_bookings" ADD COLUMN "recurring_group_id" UUID;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "recurring_booking_groups" (
|
||||
"id" UUID NOT NULL,
|
||||
"complex_id" UUID NOT NULL,
|
||||
"court_id" UUID NOT NULL,
|
||||
"start_time" VARCHAR(5) NOT NULL,
|
||||
"end_time" VARCHAR(5) NOT NULL,
|
||||
"day_of_week" "DayOfWeek" NOT NULL,
|
||||
"start_date" DATE NOT NULL,
|
||||
"end_date" DATE,
|
||||
"status" "RecurringBookingGroupStatus" NOT NULL DEFAULT 'ACTIVE',
|
||||
"customer_name" VARCHAR(120) NOT NULL,
|
||||
"customer_phone" VARCHAR(30) NOT NULL,
|
||||
"customer_email" VARCHAR(254) NOT NULL,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "recurring_booking_groups_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "recurring_booking_groups_complex_id_idx" ON "recurring_booking_groups"("complex_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "recurring_booking_groups_court_id_idx" ON "recurring_booking_groups"("court_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "recurring_booking_groups_status_idx" ON "recurring_booking_groups"("status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "court_bookings_recurring_group_id_idx" ON "court_bookings"("recurring_group_id");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "court_bookings" ADD CONSTRAINT "court_bookings_recurring_group_id_fkey" FOREIGN KEY ("recurring_group_id") REFERENCES "recurring_booking_groups"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "recurring_booking_groups" ADD CONSTRAINT "recurring_booking_groups_court_id_fkey" FOREIGN KEY ("court_id") REFERENCES "courts"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "recurring_booking_groups" ADD CONSTRAINT "recurring_booking_groups_complex_id_fkey" FOREIGN KEY ("complex_id") REFERENCES "complexes"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,4 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "court_booking_logs" ADD COLUMN "previous_court_id" UUID;
|
||||
ALTER TABLE "court_booking_logs" ADD COLUMN "previous_start_time" VARCHAR(5);
|
||||
ALTER TABLE "court_booking_logs" ADD COLUMN "previous_end_time" VARCHAR(5);
|
||||
@@ -21,6 +21,7 @@ export const planSeeds = [
|
||||
publicBookingPage: true,
|
||||
advancedReports: false,
|
||||
whatsappReminders: false,
|
||||
fixedSlots: false,
|
||||
},
|
||||
pricing: {
|
||||
overrides: {
|
||||
@@ -51,6 +52,7 @@ export const planSeeds = [
|
||||
publicBookingPage: true,
|
||||
advancedReports: true,
|
||||
whatsappReminders: true,
|
||||
fixedSlots: true,
|
||||
},
|
||||
pricing: {
|
||||
overrides: {
|
||||
@@ -81,6 +83,7 @@ export const planSeeds = [
|
||||
publicBookingPage: true,
|
||||
advancedReports: true,
|
||||
whatsappReminders: true,
|
||||
fixedSlots: true,
|
||||
},
|
||||
pricing: {
|
||||
overrides: {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -87,6 +87,11 @@ export type CourtBooking = Prisma.CourtBookingModel
|
||||
*
|
||||
*/
|
||||
export type CourtBookingLog = Prisma.CourtBookingLogModel
|
||||
/**
|
||||
* Model RecurringBookingGroup
|
||||
*
|
||||
*/
|
||||
export type RecurringBookingGroup = Prisma.RecurringBookingGroupModel
|
||||
/**
|
||||
* Model PasswordResetRequest
|
||||
*
|
||||
|
||||
@@ -111,6 +111,11 @@ export type CourtBooking = Prisma.CourtBookingModel
|
||||
*
|
||||
*/
|
||||
export type CourtBookingLog = Prisma.CourtBookingLogModel
|
||||
/**
|
||||
* Model RecurringBookingGroup
|
||||
*
|
||||
*/
|
||||
export type RecurringBookingGroup = Prisma.RecurringBookingGroupModel
|
||||
/**
|
||||
* Model PasswordResetRequest
|
||||
*
|
||||
|
||||
@@ -314,6 +314,18 @@ export type EnumCourtBookingStatusFilter<$PrismaModel = never> = {
|
||||
not?: Prisma.NestedEnumCourtBookingStatusFilter<$PrismaModel> | $Enums.CourtBookingStatus
|
||||
}
|
||||
|
||||
export type UuidNullableFilter<$PrismaModel = never> = {
|
||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
|
||||
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
|
||||
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
|
||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
mode?: Prisma.QueryMode
|
||||
not?: Prisma.NestedUuidNullableFilter<$PrismaModel> | string | null
|
||||
}
|
||||
|
||||
export type EnumCourtBookingStatusWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: $Enums.CourtBookingStatus | Prisma.EnumCourtBookingStatusFieldRefInput<$PrismaModel>
|
||||
in?: $Enums.CourtBookingStatus[] | Prisma.ListEnumCourtBookingStatusFieldRefInput<$PrismaModel>
|
||||
@@ -324,6 +336,38 @@ export type EnumCourtBookingStatusWithAggregatesFilter<$PrismaModel = never> = {
|
||||
_max?: Prisma.NestedEnumCourtBookingStatusFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type UuidNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
|
||||
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
|
||||
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
|
||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
mode?: Prisma.QueryMode
|
||||
not?: Prisma.NestedUuidNullableWithAggregatesFilter<$PrismaModel> | string | null
|
||||
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedStringNullableFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedStringNullableFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type EnumRecurringBookingGroupStatusFilter<$PrismaModel = never> = {
|
||||
equals?: $Enums.RecurringBookingGroupStatus | Prisma.EnumRecurringBookingGroupStatusFieldRefInput<$PrismaModel>
|
||||
in?: $Enums.RecurringBookingGroupStatus[] | Prisma.ListEnumRecurringBookingGroupStatusFieldRefInput<$PrismaModel>
|
||||
notIn?: $Enums.RecurringBookingGroupStatus[] | Prisma.ListEnumRecurringBookingGroupStatusFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedEnumRecurringBookingGroupStatusFilter<$PrismaModel> | $Enums.RecurringBookingGroupStatus
|
||||
}
|
||||
|
||||
export type EnumRecurringBookingGroupStatusWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: $Enums.RecurringBookingGroupStatus | Prisma.EnumRecurringBookingGroupStatusFieldRefInput<$PrismaModel>
|
||||
in?: $Enums.RecurringBookingGroupStatus[] | Prisma.ListEnumRecurringBookingGroupStatusFieldRefInput<$PrismaModel>
|
||||
notIn?: $Enums.RecurringBookingGroupStatus[] | Prisma.ListEnumRecurringBookingGroupStatusFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedEnumRecurringBookingGroupStatusWithAggregatesFilter<$PrismaModel> | $Enums.RecurringBookingGroupStatus
|
||||
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedEnumRecurringBookingGroupStatusFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedEnumRecurringBookingGroupStatusFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type JsonFilter<$PrismaModel = never> =
|
||||
| Prisma.PatchUndefined<
|
||||
Prisma.Either<Required<JsonFilterBase<$PrismaModel>>, Exclude<keyof Required<JsonFilterBase<$PrismaModel>>, 'path'>>,
|
||||
@@ -686,6 +730,17 @@ export type NestedEnumCourtBookingStatusFilter<$PrismaModel = never> = {
|
||||
not?: Prisma.NestedEnumCourtBookingStatusFilter<$PrismaModel> | $Enums.CourtBookingStatus
|
||||
}
|
||||
|
||||
export type NestedUuidNullableFilter<$PrismaModel = never> = {
|
||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
|
||||
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
|
||||
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
|
||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedUuidNullableFilter<$PrismaModel> | string | null
|
||||
}
|
||||
|
||||
export type NestedEnumCourtBookingStatusWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: $Enums.CourtBookingStatus | Prisma.EnumCourtBookingStatusFieldRefInput<$PrismaModel>
|
||||
in?: $Enums.CourtBookingStatus[] | Prisma.ListEnumCourtBookingStatusFieldRefInput<$PrismaModel>
|
||||
@@ -696,6 +751,37 @@ export type NestedEnumCourtBookingStatusWithAggregatesFilter<$PrismaModel = neve
|
||||
_max?: Prisma.NestedEnumCourtBookingStatusFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type NestedUuidNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
|
||||
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
|
||||
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
|
||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedUuidNullableWithAggregatesFilter<$PrismaModel> | string | null
|
||||
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedStringNullableFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedStringNullableFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type NestedEnumRecurringBookingGroupStatusFilter<$PrismaModel = never> = {
|
||||
equals?: $Enums.RecurringBookingGroupStatus | Prisma.EnumRecurringBookingGroupStatusFieldRefInput<$PrismaModel>
|
||||
in?: $Enums.RecurringBookingGroupStatus[] | Prisma.ListEnumRecurringBookingGroupStatusFieldRefInput<$PrismaModel>
|
||||
notIn?: $Enums.RecurringBookingGroupStatus[] | Prisma.ListEnumRecurringBookingGroupStatusFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedEnumRecurringBookingGroupStatusFilter<$PrismaModel> | $Enums.RecurringBookingGroupStatus
|
||||
}
|
||||
|
||||
export type NestedEnumRecurringBookingGroupStatusWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: $Enums.RecurringBookingGroupStatus | Prisma.EnumRecurringBookingGroupStatusFieldRefInput<$PrismaModel>
|
||||
in?: $Enums.RecurringBookingGroupStatus[] | Prisma.ListEnumRecurringBookingGroupStatusFieldRefInput<$PrismaModel>
|
||||
notIn?: $Enums.RecurringBookingGroupStatus[] | Prisma.ListEnumRecurringBookingGroupStatusFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedEnumRecurringBookingGroupStatusWithAggregatesFilter<$PrismaModel> | $Enums.RecurringBookingGroupStatus
|
||||
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedEnumRecurringBookingGroupStatusFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedEnumRecurringBookingGroupStatusFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type NestedJsonFilter<$PrismaModel = never> =
|
||||
| Prisma.PatchUndefined<
|
||||
Prisma.Either<Required<NestedJsonFilterBase<$PrismaModel>>, Exclude<keyof Required<NestedJsonFilterBase<$PrismaModel>>, 'path'>>,
|
||||
|
||||
@@ -38,3 +38,11 @@ export const CourtBookingStatus = {
|
||||
} as const
|
||||
|
||||
export type CourtBookingStatus = (typeof CourtBookingStatus)[keyof typeof CourtBookingStatus]
|
||||
|
||||
|
||||
export const RecurringBookingGroupStatus = {
|
||||
ACTIVE: 'ACTIVE',
|
||||
CANCELLED: 'CANCELLED'
|
||||
} as const
|
||||
|
||||
export type RecurringBookingGroupStatus = (typeof RecurringBookingGroupStatus)[keyof typeof RecurringBookingGroupStatus]
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -398,6 +398,7 @@ export const ModelName = {
|
||||
CourtPriceRule: 'CourtPriceRule',
|
||||
CourtBooking: 'CourtBooking',
|
||||
CourtBookingLog: 'CourtBookingLog',
|
||||
RecurringBookingGroup: 'RecurringBookingGroup',
|
||||
PasswordResetRequest: 'PasswordResetRequest',
|
||||
Plan: 'Plan'
|
||||
} as const
|
||||
@@ -415,7 +416,7 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
|
||||
omit: GlobalOmitOptions
|
||||
}
|
||||
meta: {
|
||||
modelProps: "user" | "session" | "account" | "verification" | "complex" | "complexUser" | "complexInvitation" | "sport" | "court" | "courtMaintenance" | "courtAvailability" | "courtPriceRule" | "courtBooking" | "courtBookingLog" | "passwordResetRequest" | "plan"
|
||||
modelProps: "user" | "session" | "account" | "verification" | "complex" | "complexUser" | "complexInvitation" | "sport" | "court" | "courtMaintenance" | "courtAvailability" | "courtPriceRule" | "courtBooking" | "courtBookingLog" | "recurringBookingGroup" | "passwordResetRequest" | "plan"
|
||||
txIsolationLevel: TransactionIsolationLevel
|
||||
}
|
||||
model: {
|
||||
@@ -1455,6 +1456,80 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
|
||||
}
|
||||
}
|
||||
}
|
||||
RecurringBookingGroup: {
|
||||
payload: Prisma.$RecurringBookingGroupPayload<ExtArgs>
|
||||
fields: Prisma.RecurringBookingGroupFieldRefs
|
||||
operations: {
|
||||
findUnique: {
|
||||
args: Prisma.RecurringBookingGroupFindUniqueArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$RecurringBookingGroupPayload> | null
|
||||
}
|
||||
findUniqueOrThrow: {
|
||||
args: Prisma.RecurringBookingGroupFindUniqueOrThrowArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$RecurringBookingGroupPayload>
|
||||
}
|
||||
findFirst: {
|
||||
args: Prisma.RecurringBookingGroupFindFirstArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$RecurringBookingGroupPayload> | null
|
||||
}
|
||||
findFirstOrThrow: {
|
||||
args: Prisma.RecurringBookingGroupFindFirstOrThrowArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$RecurringBookingGroupPayload>
|
||||
}
|
||||
findMany: {
|
||||
args: Prisma.RecurringBookingGroupFindManyArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$RecurringBookingGroupPayload>[]
|
||||
}
|
||||
create: {
|
||||
args: Prisma.RecurringBookingGroupCreateArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$RecurringBookingGroupPayload>
|
||||
}
|
||||
createMany: {
|
||||
args: Prisma.RecurringBookingGroupCreateManyArgs<ExtArgs>
|
||||
result: BatchPayload
|
||||
}
|
||||
createManyAndReturn: {
|
||||
args: Prisma.RecurringBookingGroupCreateManyAndReturnArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$RecurringBookingGroupPayload>[]
|
||||
}
|
||||
delete: {
|
||||
args: Prisma.RecurringBookingGroupDeleteArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$RecurringBookingGroupPayload>
|
||||
}
|
||||
update: {
|
||||
args: Prisma.RecurringBookingGroupUpdateArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$RecurringBookingGroupPayload>
|
||||
}
|
||||
deleteMany: {
|
||||
args: Prisma.RecurringBookingGroupDeleteManyArgs<ExtArgs>
|
||||
result: BatchPayload
|
||||
}
|
||||
updateMany: {
|
||||
args: Prisma.RecurringBookingGroupUpdateManyArgs<ExtArgs>
|
||||
result: BatchPayload
|
||||
}
|
||||
updateManyAndReturn: {
|
||||
args: Prisma.RecurringBookingGroupUpdateManyAndReturnArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$RecurringBookingGroupPayload>[]
|
||||
}
|
||||
upsert: {
|
||||
args: Prisma.RecurringBookingGroupUpsertArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$RecurringBookingGroupPayload>
|
||||
}
|
||||
aggregate: {
|
||||
args: Prisma.RecurringBookingGroupAggregateArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.Optional<Prisma.AggregateRecurringBookingGroup>
|
||||
}
|
||||
groupBy: {
|
||||
args: Prisma.RecurringBookingGroupGroupByArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.Optional<Prisma.RecurringBookingGroupGroupByOutputType>[]
|
||||
}
|
||||
count: {
|
||||
args: Prisma.RecurringBookingGroupCountArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.Optional<Prisma.RecurringBookingGroupCountAggregateOutputType> | number
|
||||
}
|
||||
}
|
||||
}
|
||||
PasswordResetRequest: {
|
||||
payload: Prisma.$PasswordResetRequestPayload<ExtArgs>
|
||||
fields: Prisma.PasswordResetRequestFieldRefs
|
||||
@@ -1832,6 +1907,7 @@ export const CourtBookingScalarFieldEnum = {
|
||||
customerPhone: 'customerPhone',
|
||||
customerEmail: 'customerEmail',
|
||||
status: 'status',
|
||||
recurringGroupId: 'recurringGroupId',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
} as const
|
||||
@@ -1848,6 +1924,9 @@ export const CourtBookingLogScalarFieldEnum = {
|
||||
endTime: 'endTime',
|
||||
previousStatus: 'previousStatus',
|
||||
newStatus: 'newStatus',
|
||||
previousCourtId: 'previousCourtId',
|
||||
previousStartTime: 'previousStartTime',
|
||||
previousEndTime: 'previousEndTime',
|
||||
customerName: 'customerName',
|
||||
customerPhone: 'customerPhone',
|
||||
customerEmail: 'customerEmail',
|
||||
@@ -1857,6 +1936,26 @@ export const CourtBookingLogScalarFieldEnum = {
|
||||
export type CourtBookingLogScalarFieldEnum = (typeof CourtBookingLogScalarFieldEnum)[keyof typeof CourtBookingLogScalarFieldEnum]
|
||||
|
||||
|
||||
export const RecurringBookingGroupScalarFieldEnum = {
|
||||
id: 'id',
|
||||
complexId: 'complexId',
|
||||
courtId: 'courtId',
|
||||
startTime: 'startTime',
|
||||
endTime: 'endTime',
|
||||
dayOfWeek: 'dayOfWeek',
|
||||
startDate: 'startDate',
|
||||
endDate: 'endDate',
|
||||
status: 'status',
|
||||
customerName: 'customerName',
|
||||
customerPhone: 'customerPhone',
|
||||
customerEmail: 'customerEmail',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
} as const
|
||||
|
||||
export type RecurringBookingGroupScalarFieldEnum = (typeof RecurringBookingGroupScalarFieldEnum)[keyof typeof RecurringBookingGroupScalarFieldEnum]
|
||||
|
||||
|
||||
export const PasswordResetRequestScalarFieldEnum = {
|
||||
id: 'id',
|
||||
email: 'email',
|
||||
@@ -2049,6 +2148,20 @@ export type ListEnumCourtBookingStatusFieldRefInput<$PrismaModel> = FieldRefInpu
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Reference to a field of type 'RecurringBookingGroupStatus'
|
||||
*/
|
||||
export type EnumRecurringBookingGroupStatusFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'RecurringBookingGroupStatus'>
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Reference to a field of type 'RecurringBookingGroupStatus[]'
|
||||
*/
|
||||
export type ListEnumRecurringBookingGroupStatusFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'RecurringBookingGroupStatus[]'>
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Reference to a field of type 'Json'
|
||||
*/
|
||||
@@ -2171,6 +2284,7 @@ export type GlobalOmitConfig = {
|
||||
courtPriceRule?: Prisma.CourtPriceRuleOmit
|
||||
courtBooking?: Prisma.CourtBookingOmit
|
||||
courtBookingLog?: Prisma.CourtBookingLogOmit
|
||||
recurringBookingGroup?: Prisma.RecurringBookingGroupOmit
|
||||
passwordResetRequest?: Prisma.PasswordResetRequestOmit
|
||||
plan?: Prisma.PlanOmit
|
||||
}
|
||||
|
||||
@@ -65,6 +65,7 @@ export const ModelName = {
|
||||
CourtPriceRule: 'CourtPriceRule',
|
||||
CourtBooking: 'CourtBooking',
|
||||
CourtBookingLog: 'CourtBookingLog',
|
||||
RecurringBookingGroup: 'RecurringBookingGroup',
|
||||
PasswordResetRequest: 'PasswordResetRequest',
|
||||
Plan: 'Plan'
|
||||
} as const
|
||||
@@ -275,6 +276,7 @@ export const CourtBookingScalarFieldEnum = {
|
||||
customerPhone: 'customerPhone',
|
||||
customerEmail: 'customerEmail',
|
||||
status: 'status',
|
||||
recurringGroupId: 'recurringGroupId',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
} as const
|
||||
@@ -291,6 +293,9 @@ export const CourtBookingLogScalarFieldEnum = {
|
||||
endTime: 'endTime',
|
||||
previousStatus: 'previousStatus',
|
||||
newStatus: 'newStatus',
|
||||
previousCourtId: 'previousCourtId',
|
||||
previousStartTime: 'previousStartTime',
|
||||
previousEndTime: 'previousEndTime',
|
||||
customerName: 'customerName',
|
||||
customerPhone: 'customerPhone',
|
||||
customerEmail: 'customerEmail',
|
||||
@@ -300,6 +305,26 @@ export const CourtBookingLogScalarFieldEnum = {
|
||||
export type CourtBookingLogScalarFieldEnum = (typeof CourtBookingLogScalarFieldEnum)[keyof typeof CourtBookingLogScalarFieldEnum]
|
||||
|
||||
|
||||
export const RecurringBookingGroupScalarFieldEnum = {
|
||||
id: 'id',
|
||||
complexId: 'complexId',
|
||||
courtId: 'courtId',
|
||||
startTime: 'startTime',
|
||||
endTime: 'endTime',
|
||||
dayOfWeek: 'dayOfWeek',
|
||||
startDate: 'startDate',
|
||||
endDate: 'endDate',
|
||||
status: 'status',
|
||||
customerName: 'customerName',
|
||||
customerPhone: 'customerPhone',
|
||||
customerEmail: 'customerEmail',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
} as const
|
||||
|
||||
export type RecurringBookingGroupScalarFieldEnum = (typeof RecurringBookingGroupScalarFieldEnum)[keyof typeof RecurringBookingGroupScalarFieldEnum]
|
||||
|
||||
|
||||
export const PasswordResetRequestScalarFieldEnum = {
|
||||
id: 'id',
|
||||
email: 'email',
|
||||
|
||||
@@ -22,6 +22,7 @@ export type * from './models/CourtAvailability'
|
||||
export type * from './models/CourtPriceRule'
|
||||
export type * from './models/CourtBooking'
|
||||
export type * from './models/CourtBookingLog'
|
||||
export type * from './models/RecurringBookingGroup'
|
||||
export type * from './models/PasswordResetRequest'
|
||||
export type * from './models/Plan'
|
||||
export type * from './commonInputTypes'
|
||||
@@ -234,6 +234,7 @@ export type ComplexWhereInput = {
|
||||
users?: Prisma.ComplexUserListRelationFilter
|
||||
invitations?: Prisma.ComplexInvitationListRelationFilter
|
||||
courts?: Prisma.CourtListRelationFilter
|
||||
recurringGroups?: Prisma.RecurringBookingGroupListRelationFilter
|
||||
}
|
||||
|
||||
export type ComplexOrderByWithRelationInput = {
|
||||
@@ -252,6 +253,7 @@ export type ComplexOrderByWithRelationInput = {
|
||||
users?: Prisma.ComplexUserOrderByRelationAggregateInput
|
||||
invitations?: Prisma.ComplexInvitationOrderByRelationAggregateInput
|
||||
courts?: Prisma.CourtOrderByRelationAggregateInput
|
||||
recurringGroups?: Prisma.RecurringBookingGroupOrderByRelationAggregateInput
|
||||
}
|
||||
|
||||
export type ComplexWhereUniqueInput = Prisma.AtLeast<{
|
||||
@@ -273,6 +275,7 @@ export type ComplexWhereUniqueInput = Prisma.AtLeast<{
|
||||
users?: Prisma.ComplexUserListRelationFilter
|
||||
invitations?: Prisma.ComplexInvitationListRelationFilter
|
||||
courts?: Prisma.CourtListRelationFilter
|
||||
recurringGroups?: Prisma.RecurringBookingGroupListRelationFilter
|
||||
}, "id" | "complexSlug">
|
||||
|
||||
export type ComplexOrderByWithAggregationInput = {
|
||||
@@ -324,6 +327,7 @@ export type ComplexCreateInput = {
|
||||
users?: Prisma.ComplexUserCreateNestedManyWithoutComplexInput
|
||||
invitations?: Prisma.ComplexInvitationCreateNestedManyWithoutComplexInput
|
||||
courts?: Prisma.CourtCreateNestedManyWithoutComplexInput
|
||||
recurringGroups?: Prisma.RecurringBookingGroupCreateNestedManyWithoutComplexInput
|
||||
}
|
||||
|
||||
export type ComplexUncheckedCreateInput = {
|
||||
@@ -341,6 +345,7 @@ export type ComplexUncheckedCreateInput = {
|
||||
users?: Prisma.ComplexUserUncheckedCreateNestedManyWithoutComplexInput
|
||||
invitations?: Prisma.ComplexInvitationUncheckedCreateNestedManyWithoutComplexInput
|
||||
courts?: Prisma.CourtUncheckedCreateNestedManyWithoutComplexInput
|
||||
recurringGroups?: Prisma.RecurringBookingGroupUncheckedCreateNestedManyWithoutComplexInput
|
||||
}
|
||||
|
||||
export type ComplexUpdateInput = {
|
||||
@@ -358,6 +363,7 @@ export type ComplexUpdateInput = {
|
||||
users?: Prisma.ComplexUserUpdateManyWithoutComplexNestedInput
|
||||
invitations?: Prisma.ComplexInvitationUpdateManyWithoutComplexNestedInput
|
||||
courts?: Prisma.CourtUpdateManyWithoutComplexNestedInput
|
||||
recurringGroups?: Prisma.RecurringBookingGroupUpdateManyWithoutComplexNestedInput
|
||||
}
|
||||
|
||||
export type ComplexUncheckedUpdateInput = {
|
||||
@@ -375,6 +381,7 @@ export type ComplexUncheckedUpdateInput = {
|
||||
users?: Prisma.ComplexUserUncheckedUpdateManyWithoutComplexNestedInput
|
||||
invitations?: Prisma.ComplexInvitationUncheckedUpdateManyWithoutComplexNestedInput
|
||||
courts?: Prisma.CourtUncheckedUpdateManyWithoutComplexNestedInput
|
||||
recurringGroups?: Prisma.RecurringBookingGroupUncheckedUpdateManyWithoutComplexNestedInput
|
||||
}
|
||||
|
||||
export type ComplexCreateManyInput = {
|
||||
@@ -517,6 +524,20 @@ export type ComplexUpdateOneRequiredWithoutCourtsNestedInput = {
|
||||
update?: Prisma.XOR<Prisma.XOR<Prisma.ComplexUpdateToOneWithWhereWithoutCourtsInput, Prisma.ComplexUpdateWithoutCourtsInput>, Prisma.ComplexUncheckedUpdateWithoutCourtsInput>
|
||||
}
|
||||
|
||||
export type ComplexCreateNestedOneWithoutRecurringGroupsInput = {
|
||||
create?: Prisma.XOR<Prisma.ComplexCreateWithoutRecurringGroupsInput, Prisma.ComplexUncheckedCreateWithoutRecurringGroupsInput>
|
||||
connectOrCreate?: Prisma.ComplexCreateOrConnectWithoutRecurringGroupsInput
|
||||
connect?: Prisma.ComplexWhereUniqueInput
|
||||
}
|
||||
|
||||
export type ComplexUpdateOneRequiredWithoutRecurringGroupsNestedInput = {
|
||||
create?: Prisma.XOR<Prisma.ComplexCreateWithoutRecurringGroupsInput, Prisma.ComplexUncheckedCreateWithoutRecurringGroupsInput>
|
||||
connectOrCreate?: Prisma.ComplexCreateOrConnectWithoutRecurringGroupsInput
|
||||
upsert?: Prisma.ComplexUpsertWithoutRecurringGroupsInput
|
||||
connect?: Prisma.ComplexWhereUniqueInput
|
||||
update?: Prisma.XOR<Prisma.XOR<Prisma.ComplexUpdateToOneWithWhereWithoutRecurringGroupsInput, Prisma.ComplexUpdateWithoutRecurringGroupsInput>, Prisma.ComplexUncheckedUpdateWithoutRecurringGroupsInput>
|
||||
}
|
||||
|
||||
export type ComplexCreateNestedManyWithoutPlanInput = {
|
||||
create?: Prisma.XOR<Prisma.ComplexCreateWithoutPlanInput, Prisma.ComplexUncheckedCreateWithoutPlanInput> | Prisma.ComplexCreateWithoutPlanInput[] | Prisma.ComplexUncheckedCreateWithoutPlanInput[]
|
||||
connectOrCreate?: Prisma.ComplexCreateOrConnectWithoutPlanInput | Prisma.ComplexCreateOrConnectWithoutPlanInput[]
|
||||
@@ -573,6 +594,7 @@ export type ComplexCreateWithoutUsersInput = {
|
||||
plan?: Prisma.PlanCreateNestedOneWithoutComplexesInput
|
||||
invitations?: Prisma.ComplexInvitationCreateNestedManyWithoutComplexInput
|
||||
courts?: Prisma.CourtCreateNestedManyWithoutComplexInput
|
||||
recurringGroups?: Prisma.RecurringBookingGroupCreateNestedManyWithoutComplexInput
|
||||
}
|
||||
|
||||
export type ComplexUncheckedCreateWithoutUsersInput = {
|
||||
@@ -589,6 +611,7 @@ export type ComplexUncheckedCreateWithoutUsersInput = {
|
||||
updatedAt?: Date | string
|
||||
invitations?: Prisma.ComplexInvitationUncheckedCreateNestedManyWithoutComplexInput
|
||||
courts?: Prisma.CourtUncheckedCreateNestedManyWithoutComplexInput
|
||||
recurringGroups?: Prisma.RecurringBookingGroupUncheckedCreateNestedManyWithoutComplexInput
|
||||
}
|
||||
|
||||
export type ComplexCreateOrConnectWithoutUsersInput = {
|
||||
@@ -621,6 +644,7 @@ export type ComplexUpdateWithoutUsersInput = {
|
||||
plan?: Prisma.PlanUpdateOneWithoutComplexesNestedInput
|
||||
invitations?: Prisma.ComplexInvitationUpdateManyWithoutComplexNestedInput
|
||||
courts?: Prisma.CourtUpdateManyWithoutComplexNestedInput
|
||||
recurringGroups?: Prisma.RecurringBookingGroupUpdateManyWithoutComplexNestedInput
|
||||
}
|
||||
|
||||
export type ComplexUncheckedUpdateWithoutUsersInput = {
|
||||
@@ -637,6 +661,7 @@ export type ComplexUncheckedUpdateWithoutUsersInput = {
|
||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
invitations?: Prisma.ComplexInvitationUncheckedUpdateManyWithoutComplexNestedInput
|
||||
courts?: Prisma.CourtUncheckedUpdateManyWithoutComplexNestedInput
|
||||
recurringGroups?: Prisma.RecurringBookingGroupUncheckedUpdateManyWithoutComplexNestedInput
|
||||
}
|
||||
|
||||
export type ComplexCreateWithoutInvitationsInput = {
|
||||
@@ -653,6 +678,7 @@ export type ComplexCreateWithoutInvitationsInput = {
|
||||
plan?: Prisma.PlanCreateNestedOneWithoutComplexesInput
|
||||
users?: Prisma.ComplexUserCreateNestedManyWithoutComplexInput
|
||||
courts?: Prisma.CourtCreateNestedManyWithoutComplexInput
|
||||
recurringGroups?: Prisma.RecurringBookingGroupCreateNestedManyWithoutComplexInput
|
||||
}
|
||||
|
||||
export type ComplexUncheckedCreateWithoutInvitationsInput = {
|
||||
@@ -669,6 +695,7 @@ export type ComplexUncheckedCreateWithoutInvitationsInput = {
|
||||
updatedAt?: Date | string
|
||||
users?: Prisma.ComplexUserUncheckedCreateNestedManyWithoutComplexInput
|
||||
courts?: Prisma.CourtUncheckedCreateNestedManyWithoutComplexInput
|
||||
recurringGroups?: Prisma.RecurringBookingGroupUncheckedCreateNestedManyWithoutComplexInput
|
||||
}
|
||||
|
||||
export type ComplexCreateOrConnectWithoutInvitationsInput = {
|
||||
@@ -701,6 +728,7 @@ export type ComplexUpdateWithoutInvitationsInput = {
|
||||
plan?: Prisma.PlanUpdateOneWithoutComplexesNestedInput
|
||||
users?: Prisma.ComplexUserUpdateManyWithoutComplexNestedInput
|
||||
courts?: Prisma.CourtUpdateManyWithoutComplexNestedInput
|
||||
recurringGroups?: Prisma.RecurringBookingGroupUpdateManyWithoutComplexNestedInput
|
||||
}
|
||||
|
||||
export type ComplexUncheckedUpdateWithoutInvitationsInput = {
|
||||
@@ -717,6 +745,7 @@ export type ComplexUncheckedUpdateWithoutInvitationsInput = {
|
||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
users?: Prisma.ComplexUserUncheckedUpdateManyWithoutComplexNestedInput
|
||||
courts?: Prisma.CourtUncheckedUpdateManyWithoutComplexNestedInput
|
||||
recurringGroups?: Prisma.RecurringBookingGroupUncheckedUpdateManyWithoutComplexNestedInput
|
||||
}
|
||||
|
||||
export type ComplexCreateWithoutCourtsInput = {
|
||||
@@ -733,6 +762,7 @@ export type ComplexCreateWithoutCourtsInput = {
|
||||
plan?: Prisma.PlanCreateNestedOneWithoutComplexesInput
|
||||
users?: Prisma.ComplexUserCreateNestedManyWithoutComplexInput
|
||||
invitations?: Prisma.ComplexInvitationCreateNestedManyWithoutComplexInput
|
||||
recurringGroups?: Prisma.RecurringBookingGroupCreateNestedManyWithoutComplexInput
|
||||
}
|
||||
|
||||
export type ComplexUncheckedCreateWithoutCourtsInput = {
|
||||
@@ -749,6 +779,7 @@ export type ComplexUncheckedCreateWithoutCourtsInput = {
|
||||
updatedAt?: Date | string
|
||||
users?: Prisma.ComplexUserUncheckedCreateNestedManyWithoutComplexInput
|
||||
invitations?: Prisma.ComplexInvitationUncheckedCreateNestedManyWithoutComplexInput
|
||||
recurringGroups?: Prisma.RecurringBookingGroupUncheckedCreateNestedManyWithoutComplexInput
|
||||
}
|
||||
|
||||
export type ComplexCreateOrConnectWithoutCourtsInput = {
|
||||
@@ -781,6 +812,7 @@ export type ComplexUpdateWithoutCourtsInput = {
|
||||
plan?: Prisma.PlanUpdateOneWithoutComplexesNestedInput
|
||||
users?: Prisma.ComplexUserUpdateManyWithoutComplexNestedInput
|
||||
invitations?: Prisma.ComplexInvitationUpdateManyWithoutComplexNestedInput
|
||||
recurringGroups?: Prisma.RecurringBookingGroupUpdateManyWithoutComplexNestedInput
|
||||
}
|
||||
|
||||
export type ComplexUncheckedUpdateWithoutCourtsInput = {
|
||||
@@ -797,6 +829,91 @@ export type ComplexUncheckedUpdateWithoutCourtsInput = {
|
||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
users?: Prisma.ComplexUserUncheckedUpdateManyWithoutComplexNestedInput
|
||||
invitations?: Prisma.ComplexInvitationUncheckedUpdateManyWithoutComplexNestedInput
|
||||
recurringGroups?: Prisma.RecurringBookingGroupUncheckedUpdateManyWithoutComplexNestedInput
|
||||
}
|
||||
|
||||
export type ComplexCreateWithoutRecurringGroupsInput = {
|
||||
id: string
|
||||
complexName: string
|
||||
physicalAddress?: string | null
|
||||
city?: string | null
|
||||
state?: string | null
|
||||
country?: string | null
|
||||
complexSlug: string
|
||||
adminEmail: string
|
||||
createdAt?: Date | string
|
||||
updatedAt?: Date | string
|
||||
plan?: Prisma.PlanCreateNestedOneWithoutComplexesInput
|
||||
users?: Prisma.ComplexUserCreateNestedManyWithoutComplexInput
|
||||
invitations?: Prisma.ComplexInvitationCreateNestedManyWithoutComplexInput
|
||||
courts?: Prisma.CourtCreateNestedManyWithoutComplexInput
|
||||
}
|
||||
|
||||
export type ComplexUncheckedCreateWithoutRecurringGroupsInput = {
|
||||
id: string
|
||||
complexName: string
|
||||
physicalAddress?: string | null
|
||||
city?: string | null
|
||||
state?: string | null
|
||||
country?: string | null
|
||||
complexSlug: string
|
||||
adminEmail: string
|
||||
planCode?: string | null
|
||||
createdAt?: Date | string
|
||||
updatedAt?: Date | string
|
||||
users?: Prisma.ComplexUserUncheckedCreateNestedManyWithoutComplexInput
|
||||
invitations?: Prisma.ComplexInvitationUncheckedCreateNestedManyWithoutComplexInput
|
||||
courts?: Prisma.CourtUncheckedCreateNestedManyWithoutComplexInput
|
||||
}
|
||||
|
||||
export type ComplexCreateOrConnectWithoutRecurringGroupsInput = {
|
||||
where: Prisma.ComplexWhereUniqueInput
|
||||
create: Prisma.XOR<Prisma.ComplexCreateWithoutRecurringGroupsInput, Prisma.ComplexUncheckedCreateWithoutRecurringGroupsInput>
|
||||
}
|
||||
|
||||
export type ComplexUpsertWithoutRecurringGroupsInput = {
|
||||
update: Prisma.XOR<Prisma.ComplexUpdateWithoutRecurringGroupsInput, Prisma.ComplexUncheckedUpdateWithoutRecurringGroupsInput>
|
||||
create: Prisma.XOR<Prisma.ComplexCreateWithoutRecurringGroupsInput, Prisma.ComplexUncheckedCreateWithoutRecurringGroupsInput>
|
||||
where?: Prisma.ComplexWhereInput
|
||||
}
|
||||
|
||||
export type ComplexUpdateToOneWithWhereWithoutRecurringGroupsInput = {
|
||||
where?: Prisma.ComplexWhereInput
|
||||
data: Prisma.XOR<Prisma.ComplexUpdateWithoutRecurringGroupsInput, Prisma.ComplexUncheckedUpdateWithoutRecurringGroupsInput>
|
||||
}
|
||||
|
||||
export type ComplexUpdateWithoutRecurringGroupsInput = {
|
||||
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
complexName?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
physicalAddress?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
city?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
state?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
country?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
complexSlug?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
adminEmail?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
plan?: Prisma.PlanUpdateOneWithoutComplexesNestedInput
|
||||
users?: Prisma.ComplexUserUpdateManyWithoutComplexNestedInput
|
||||
invitations?: Prisma.ComplexInvitationUpdateManyWithoutComplexNestedInput
|
||||
courts?: Prisma.CourtUpdateManyWithoutComplexNestedInput
|
||||
}
|
||||
|
||||
export type ComplexUncheckedUpdateWithoutRecurringGroupsInput = {
|
||||
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
complexName?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
physicalAddress?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
city?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
state?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
country?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
complexSlug?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
adminEmail?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
planCode?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
users?: Prisma.ComplexUserUncheckedUpdateManyWithoutComplexNestedInput
|
||||
invitations?: Prisma.ComplexInvitationUncheckedUpdateManyWithoutComplexNestedInput
|
||||
courts?: Prisma.CourtUncheckedUpdateManyWithoutComplexNestedInput
|
||||
}
|
||||
|
||||
export type ComplexCreateWithoutPlanInput = {
|
||||
@@ -813,6 +930,7 @@ export type ComplexCreateWithoutPlanInput = {
|
||||
users?: Prisma.ComplexUserCreateNestedManyWithoutComplexInput
|
||||
invitations?: Prisma.ComplexInvitationCreateNestedManyWithoutComplexInput
|
||||
courts?: Prisma.CourtCreateNestedManyWithoutComplexInput
|
||||
recurringGroups?: Prisma.RecurringBookingGroupCreateNestedManyWithoutComplexInput
|
||||
}
|
||||
|
||||
export type ComplexUncheckedCreateWithoutPlanInput = {
|
||||
@@ -829,6 +947,7 @@ export type ComplexUncheckedCreateWithoutPlanInput = {
|
||||
users?: Prisma.ComplexUserUncheckedCreateNestedManyWithoutComplexInput
|
||||
invitations?: Prisma.ComplexInvitationUncheckedCreateNestedManyWithoutComplexInput
|
||||
courts?: Prisma.CourtUncheckedCreateNestedManyWithoutComplexInput
|
||||
recurringGroups?: Prisma.RecurringBookingGroupUncheckedCreateNestedManyWithoutComplexInput
|
||||
}
|
||||
|
||||
export type ComplexCreateOrConnectWithoutPlanInput = {
|
||||
@@ -901,6 +1020,7 @@ export type ComplexUpdateWithoutPlanInput = {
|
||||
users?: Prisma.ComplexUserUpdateManyWithoutComplexNestedInput
|
||||
invitations?: Prisma.ComplexInvitationUpdateManyWithoutComplexNestedInput
|
||||
courts?: Prisma.CourtUpdateManyWithoutComplexNestedInput
|
||||
recurringGroups?: Prisma.RecurringBookingGroupUpdateManyWithoutComplexNestedInput
|
||||
}
|
||||
|
||||
export type ComplexUncheckedUpdateWithoutPlanInput = {
|
||||
@@ -917,6 +1037,7 @@ export type ComplexUncheckedUpdateWithoutPlanInput = {
|
||||
users?: Prisma.ComplexUserUncheckedUpdateManyWithoutComplexNestedInput
|
||||
invitations?: Prisma.ComplexInvitationUncheckedUpdateManyWithoutComplexNestedInput
|
||||
courts?: Prisma.CourtUncheckedUpdateManyWithoutComplexNestedInput
|
||||
recurringGroups?: Prisma.RecurringBookingGroupUncheckedUpdateManyWithoutComplexNestedInput
|
||||
}
|
||||
|
||||
export type ComplexUncheckedUpdateManyWithoutPlanInput = {
|
||||
@@ -941,12 +1062,14 @@ export type ComplexCountOutputType = {
|
||||
users: number
|
||||
invitations: number
|
||||
courts: number
|
||||
recurringGroups: number
|
||||
}
|
||||
|
||||
export type ComplexCountOutputTypeSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||
users?: boolean | ComplexCountOutputTypeCountUsersArgs
|
||||
invitations?: boolean | ComplexCountOutputTypeCountInvitationsArgs
|
||||
courts?: boolean | ComplexCountOutputTypeCountCourtsArgs
|
||||
recurringGroups?: boolean | ComplexCountOutputTypeCountRecurringGroupsArgs
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -980,6 +1103,13 @@ export type ComplexCountOutputTypeCountCourtsArgs<ExtArgs extends runtime.Types.
|
||||
where?: Prisma.CourtWhereInput
|
||||
}
|
||||
|
||||
/**
|
||||
* ComplexCountOutputType without action
|
||||
*/
|
||||
export type ComplexCountOutputTypeCountRecurringGroupsArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||
where?: Prisma.RecurringBookingGroupWhereInput
|
||||
}
|
||||
|
||||
|
||||
export type ComplexSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
|
||||
id?: boolean
|
||||
@@ -997,6 +1127,7 @@ export type ComplexSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs
|
||||
users?: boolean | Prisma.Complex$usersArgs<ExtArgs>
|
||||
invitations?: boolean | Prisma.Complex$invitationsArgs<ExtArgs>
|
||||
courts?: boolean | Prisma.Complex$courtsArgs<ExtArgs>
|
||||
recurringGroups?: boolean | Prisma.Complex$recurringGroupsArgs<ExtArgs>
|
||||
_count?: boolean | Prisma.ComplexCountOutputTypeDefaultArgs<ExtArgs>
|
||||
}, ExtArgs["result"]["complex"]>
|
||||
|
||||
@@ -1050,6 +1181,7 @@ export type ComplexInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs
|
||||
users?: boolean | Prisma.Complex$usersArgs<ExtArgs>
|
||||
invitations?: boolean | Prisma.Complex$invitationsArgs<ExtArgs>
|
||||
courts?: boolean | Prisma.Complex$courtsArgs<ExtArgs>
|
||||
recurringGroups?: boolean | Prisma.Complex$recurringGroupsArgs<ExtArgs>
|
||||
_count?: boolean | Prisma.ComplexCountOutputTypeDefaultArgs<ExtArgs>
|
||||
}
|
||||
export type ComplexIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||
@@ -1066,6 +1198,7 @@ export type $ComplexPayload<ExtArgs extends runtime.Types.Extensions.InternalArg
|
||||
users: Prisma.$ComplexUserPayload<ExtArgs>[]
|
||||
invitations: Prisma.$ComplexInvitationPayload<ExtArgs>[]
|
||||
courts: Prisma.$CourtPayload<ExtArgs>[]
|
||||
recurringGroups: Prisma.$RecurringBookingGroupPayload<ExtArgs>[]
|
||||
}
|
||||
scalars: runtime.Types.Extensions.GetPayloadResult<{
|
||||
id: string
|
||||
@@ -1477,6 +1610,7 @@ export interface Prisma__ComplexClient<T, Null = never, ExtArgs extends runtime.
|
||||
users<T extends Prisma.Complex$usersArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Complex$usersArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$ComplexUserPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
||||
invitations<T extends Prisma.Complex$invitationsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Complex$invitationsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$ComplexInvitationPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
||||
courts<T extends Prisma.Complex$courtsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Complex$courtsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$CourtPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
||||
recurringGroups<T extends Prisma.Complex$recurringGroupsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Complex$recurringGroupsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$RecurringBookingGroupPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
||||
/**
|
||||
* Attaches callbacks for the resolution and/or rejection of the Promise.
|
||||
* @param onfulfilled The callback to execute when the Promise is resolved.
|
||||
@@ -2008,6 +2142,30 @@ export type Complex$courtsArgs<ExtArgs extends runtime.Types.Extensions.Internal
|
||||
distinct?: Prisma.CourtScalarFieldEnum | Prisma.CourtScalarFieldEnum[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Complex.recurringGroups
|
||||
*/
|
||||
export type Complex$recurringGroupsArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||
/**
|
||||
* Select specific fields to fetch from the RecurringBookingGroup
|
||||
*/
|
||||
select?: Prisma.RecurringBookingGroupSelect<ExtArgs> | null
|
||||
/**
|
||||
* Omit specific fields from the RecurringBookingGroup
|
||||
*/
|
||||
omit?: Prisma.RecurringBookingGroupOmit<ExtArgs> | null
|
||||
/**
|
||||
* Choose, which related nodes to fetch as well
|
||||
*/
|
||||
include?: Prisma.RecurringBookingGroupInclude<ExtArgs> | null
|
||||
where?: Prisma.RecurringBookingGroupWhereInput
|
||||
orderBy?: Prisma.RecurringBookingGroupOrderByWithRelationInput | Prisma.RecurringBookingGroupOrderByWithRelationInput[]
|
||||
cursor?: Prisma.RecurringBookingGroupWhereUniqueInput
|
||||
take?: number
|
||||
skip?: number
|
||||
distinct?: Prisma.RecurringBookingGroupScalarFieldEnum | Prisma.RecurringBookingGroupScalarFieldEnum[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Complex without action
|
||||
*/
|
||||
|
||||
@@ -266,6 +266,7 @@ export type CourtWhereInput = {
|
||||
priceRules?: Prisma.CourtPriceRuleListRelationFilter
|
||||
bookings?: Prisma.CourtBookingListRelationFilter
|
||||
maintenances?: Prisma.CourtMaintenanceListRelationFilter
|
||||
recurringGroups?: Prisma.RecurringBookingGroupListRelationFilter
|
||||
}
|
||||
|
||||
export type CourtOrderByWithRelationInput = {
|
||||
@@ -285,6 +286,7 @@ export type CourtOrderByWithRelationInput = {
|
||||
priceRules?: Prisma.CourtPriceRuleOrderByRelationAggregateInput
|
||||
bookings?: Prisma.CourtBookingOrderByRelationAggregateInput
|
||||
maintenances?: Prisma.CourtMaintenanceOrderByRelationAggregateInput
|
||||
recurringGroups?: Prisma.RecurringBookingGroupOrderByRelationAggregateInput
|
||||
}
|
||||
|
||||
export type CourtWhereUniqueInput = Prisma.AtLeast<{
|
||||
@@ -307,6 +309,7 @@ export type CourtWhereUniqueInput = Prisma.AtLeast<{
|
||||
priceRules?: Prisma.CourtPriceRuleListRelationFilter
|
||||
bookings?: Prisma.CourtBookingListRelationFilter
|
||||
maintenances?: Prisma.CourtMaintenanceListRelationFilter
|
||||
recurringGroups?: Prisma.RecurringBookingGroupListRelationFilter
|
||||
}, "id">
|
||||
|
||||
export type CourtOrderByWithAggregationInput = {
|
||||
@@ -358,6 +361,7 @@ export type CourtCreateInput = {
|
||||
priceRules?: Prisma.CourtPriceRuleCreateNestedManyWithoutCourtInput
|
||||
bookings?: Prisma.CourtBookingCreateNestedManyWithoutCourtInput
|
||||
maintenances?: Prisma.CourtMaintenanceCreateNestedManyWithoutCourtInput
|
||||
recurringGroups?: Prisma.RecurringBookingGroupCreateNestedManyWithoutCourtInput
|
||||
}
|
||||
|
||||
export type CourtUncheckedCreateInput = {
|
||||
@@ -375,6 +379,7 @@ export type CourtUncheckedCreateInput = {
|
||||
priceRules?: Prisma.CourtPriceRuleUncheckedCreateNestedManyWithoutCourtInput
|
||||
bookings?: Prisma.CourtBookingUncheckedCreateNestedManyWithoutCourtInput
|
||||
maintenances?: Prisma.CourtMaintenanceUncheckedCreateNestedManyWithoutCourtInput
|
||||
recurringGroups?: Prisma.RecurringBookingGroupUncheckedCreateNestedManyWithoutCourtInput
|
||||
}
|
||||
|
||||
export type CourtUpdateInput = {
|
||||
@@ -392,6 +397,7 @@ export type CourtUpdateInput = {
|
||||
priceRules?: Prisma.CourtPriceRuleUpdateManyWithoutCourtNestedInput
|
||||
bookings?: Prisma.CourtBookingUpdateManyWithoutCourtNestedInput
|
||||
maintenances?: Prisma.CourtMaintenanceUpdateManyWithoutCourtNestedInput
|
||||
recurringGroups?: Prisma.RecurringBookingGroupUpdateManyWithoutCourtNestedInput
|
||||
}
|
||||
|
||||
export type CourtUncheckedUpdateInput = {
|
||||
@@ -409,6 +415,7 @@ export type CourtUncheckedUpdateInput = {
|
||||
priceRules?: Prisma.CourtPriceRuleUncheckedUpdateManyWithoutCourtNestedInput
|
||||
bookings?: Prisma.CourtBookingUncheckedUpdateManyWithoutCourtNestedInput
|
||||
maintenances?: Prisma.CourtMaintenanceUncheckedUpdateManyWithoutCourtNestedInput
|
||||
recurringGroups?: Prisma.RecurringBookingGroupUncheckedUpdateManyWithoutCourtNestedInput
|
||||
}
|
||||
|
||||
export type CourtCreateManyInput = {
|
||||
@@ -668,6 +675,20 @@ export type CourtUpdateOneRequiredWithoutBookingsNestedInput = {
|
||||
update?: Prisma.XOR<Prisma.XOR<Prisma.CourtUpdateToOneWithWhereWithoutBookingsInput, Prisma.CourtUpdateWithoutBookingsInput>, Prisma.CourtUncheckedUpdateWithoutBookingsInput>
|
||||
}
|
||||
|
||||
export type CourtCreateNestedOneWithoutRecurringGroupsInput = {
|
||||
create?: Prisma.XOR<Prisma.CourtCreateWithoutRecurringGroupsInput, Prisma.CourtUncheckedCreateWithoutRecurringGroupsInput>
|
||||
connectOrCreate?: Prisma.CourtCreateOrConnectWithoutRecurringGroupsInput
|
||||
connect?: Prisma.CourtWhereUniqueInput
|
||||
}
|
||||
|
||||
export type CourtUpdateOneRequiredWithoutRecurringGroupsNestedInput = {
|
||||
create?: Prisma.XOR<Prisma.CourtCreateWithoutRecurringGroupsInput, Prisma.CourtUncheckedCreateWithoutRecurringGroupsInput>
|
||||
connectOrCreate?: Prisma.CourtCreateOrConnectWithoutRecurringGroupsInput
|
||||
upsert?: Prisma.CourtUpsertWithoutRecurringGroupsInput
|
||||
connect?: Prisma.CourtWhereUniqueInput
|
||||
update?: Prisma.XOR<Prisma.XOR<Prisma.CourtUpdateToOneWithWhereWithoutRecurringGroupsInput, Prisma.CourtUpdateWithoutRecurringGroupsInput>, Prisma.CourtUncheckedUpdateWithoutRecurringGroupsInput>
|
||||
}
|
||||
|
||||
export type CourtCreateWithoutComplexInput = {
|
||||
id: string
|
||||
name: string
|
||||
@@ -682,6 +703,7 @@ export type CourtCreateWithoutComplexInput = {
|
||||
priceRules?: Prisma.CourtPriceRuleCreateNestedManyWithoutCourtInput
|
||||
bookings?: Prisma.CourtBookingCreateNestedManyWithoutCourtInput
|
||||
maintenances?: Prisma.CourtMaintenanceCreateNestedManyWithoutCourtInput
|
||||
recurringGroups?: Prisma.RecurringBookingGroupCreateNestedManyWithoutCourtInput
|
||||
}
|
||||
|
||||
export type CourtUncheckedCreateWithoutComplexInput = {
|
||||
@@ -698,6 +720,7 @@ export type CourtUncheckedCreateWithoutComplexInput = {
|
||||
priceRules?: Prisma.CourtPriceRuleUncheckedCreateNestedManyWithoutCourtInput
|
||||
bookings?: Prisma.CourtBookingUncheckedCreateNestedManyWithoutCourtInput
|
||||
maintenances?: Prisma.CourtMaintenanceUncheckedCreateNestedManyWithoutCourtInput
|
||||
recurringGroups?: Prisma.RecurringBookingGroupUncheckedCreateNestedManyWithoutCourtInput
|
||||
}
|
||||
|
||||
export type CourtCreateOrConnectWithoutComplexInput = {
|
||||
@@ -756,6 +779,7 @@ export type CourtCreateWithoutSportInput = {
|
||||
priceRules?: Prisma.CourtPriceRuleCreateNestedManyWithoutCourtInput
|
||||
bookings?: Prisma.CourtBookingCreateNestedManyWithoutCourtInput
|
||||
maintenances?: Prisma.CourtMaintenanceCreateNestedManyWithoutCourtInput
|
||||
recurringGroups?: Prisma.RecurringBookingGroupCreateNestedManyWithoutCourtInput
|
||||
}
|
||||
|
||||
export type CourtUncheckedCreateWithoutSportInput = {
|
||||
@@ -772,6 +796,7 @@ export type CourtUncheckedCreateWithoutSportInput = {
|
||||
priceRules?: Prisma.CourtPriceRuleUncheckedCreateNestedManyWithoutCourtInput
|
||||
bookings?: Prisma.CourtBookingUncheckedCreateNestedManyWithoutCourtInput
|
||||
maintenances?: Prisma.CourtMaintenanceUncheckedCreateNestedManyWithoutCourtInput
|
||||
recurringGroups?: Prisma.RecurringBookingGroupUncheckedCreateNestedManyWithoutCourtInput
|
||||
}
|
||||
|
||||
export type CourtCreateOrConnectWithoutSportInput = {
|
||||
@@ -814,6 +839,7 @@ export type CourtCreateWithoutMaintenancesInput = {
|
||||
availabilities?: Prisma.CourtAvailabilityCreateNestedManyWithoutCourtInput
|
||||
priceRules?: Prisma.CourtPriceRuleCreateNestedManyWithoutCourtInput
|
||||
bookings?: Prisma.CourtBookingCreateNestedManyWithoutCourtInput
|
||||
recurringGroups?: Prisma.RecurringBookingGroupCreateNestedManyWithoutCourtInput
|
||||
}
|
||||
|
||||
export type CourtUncheckedCreateWithoutMaintenancesInput = {
|
||||
@@ -830,6 +856,7 @@ export type CourtUncheckedCreateWithoutMaintenancesInput = {
|
||||
availabilities?: Prisma.CourtAvailabilityUncheckedCreateNestedManyWithoutCourtInput
|
||||
priceRules?: Prisma.CourtPriceRuleUncheckedCreateNestedManyWithoutCourtInput
|
||||
bookings?: Prisma.CourtBookingUncheckedCreateNestedManyWithoutCourtInput
|
||||
recurringGroups?: Prisma.RecurringBookingGroupUncheckedCreateNestedManyWithoutCourtInput
|
||||
}
|
||||
|
||||
export type CourtCreateOrConnectWithoutMaintenancesInput = {
|
||||
@@ -862,6 +889,7 @@ export type CourtUpdateWithoutMaintenancesInput = {
|
||||
availabilities?: Prisma.CourtAvailabilityUpdateManyWithoutCourtNestedInput
|
||||
priceRules?: Prisma.CourtPriceRuleUpdateManyWithoutCourtNestedInput
|
||||
bookings?: Prisma.CourtBookingUpdateManyWithoutCourtNestedInput
|
||||
recurringGroups?: Prisma.RecurringBookingGroupUpdateManyWithoutCourtNestedInput
|
||||
}
|
||||
|
||||
export type CourtUncheckedUpdateWithoutMaintenancesInput = {
|
||||
@@ -878,6 +906,7 @@ export type CourtUncheckedUpdateWithoutMaintenancesInput = {
|
||||
availabilities?: Prisma.CourtAvailabilityUncheckedUpdateManyWithoutCourtNestedInput
|
||||
priceRules?: Prisma.CourtPriceRuleUncheckedUpdateManyWithoutCourtNestedInput
|
||||
bookings?: Prisma.CourtBookingUncheckedUpdateManyWithoutCourtNestedInput
|
||||
recurringGroups?: Prisma.RecurringBookingGroupUncheckedUpdateManyWithoutCourtNestedInput
|
||||
}
|
||||
|
||||
export type CourtCreateWithoutAvailabilitiesInput = {
|
||||
@@ -894,6 +923,7 @@ export type CourtCreateWithoutAvailabilitiesInput = {
|
||||
priceRules?: Prisma.CourtPriceRuleCreateNestedManyWithoutCourtInput
|
||||
bookings?: Prisma.CourtBookingCreateNestedManyWithoutCourtInput
|
||||
maintenances?: Prisma.CourtMaintenanceCreateNestedManyWithoutCourtInput
|
||||
recurringGroups?: Prisma.RecurringBookingGroupCreateNestedManyWithoutCourtInput
|
||||
}
|
||||
|
||||
export type CourtUncheckedCreateWithoutAvailabilitiesInput = {
|
||||
@@ -910,6 +940,7 @@ export type CourtUncheckedCreateWithoutAvailabilitiesInput = {
|
||||
priceRules?: Prisma.CourtPriceRuleUncheckedCreateNestedManyWithoutCourtInput
|
||||
bookings?: Prisma.CourtBookingUncheckedCreateNestedManyWithoutCourtInput
|
||||
maintenances?: Prisma.CourtMaintenanceUncheckedCreateNestedManyWithoutCourtInput
|
||||
recurringGroups?: Prisma.RecurringBookingGroupUncheckedCreateNestedManyWithoutCourtInput
|
||||
}
|
||||
|
||||
export type CourtCreateOrConnectWithoutAvailabilitiesInput = {
|
||||
@@ -942,6 +973,7 @@ export type CourtUpdateWithoutAvailabilitiesInput = {
|
||||
priceRules?: Prisma.CourtPriceRuleUpdateManyWithoutCourtNestedInput
|
||||
bookings?: Prisma.CourtBookingUpdateManyWithoutCourtNestedInput
|
||||
maintenances?: Prisma.CourtMaintenanceUpdateManyWithoutCourtNestedInput
|
||||
recurringGroups?: Prisma.RecurringBookingGroupUpdateManyWithoutCourtNestedInput
|
||||
}
|
||||
|
||||
export type CourtUncheckedUpdateWithoutAvailabilitiesInput = {
|
||||
@@ -958,6 +990,7 @@ export type CourtUncheckedUpdateWithoutAvailabilitiesInput = {
|
||||
priceRules?: Prisma.CourtPriceRuleUncheckedUpdateManyWithoutCourtNestedInput
|
||||
bookings?: Prisma.CourtBookingUncheckedUpdateManyWithoutCourtNestedInput
|
||||
maintenances?: Prisma.CourtMaintenanceUncheckedUpdateManyWithoutCourtNestedInput
|
||||
recurringGroups?: Prisma.RecurringBookingGroupUncheckedUpdateManyWithoutCourtNestedInput
|
||||
}
|
||||
|
||||
export type CourtCreateWithoutPriceRulesInput = {
|
||||
@@ -974,6 +1007,7 @@ export type CourtCreateWithoutPriceRulesInput = {
|
||||
availabilities?: Prisma.CourtAvailabilityCreateNestedManyWithoutCourtInput
|
||||
bookings?: Prisma.CourtBookingCreateNestedManyWithoutCourtInput
|
||||
maintenances?: Prisma.CourtMaintenanceCreateNestedManyWithoutCourtInput
|
||||
recurringGroups?: Prisma.RecurringBookingGroupCreateNestedManyWithoutCourtInput
|
||||
}
|
||||
|
||||
export type CourtUncheckedCreateWithoutPriceRulesInput = {
|
||||
@@ -990,6 +1024,7 @@ export type CourtUncheckedCreateWithoutPriceRulesInput = {
|
||||
availabilities?: Prisma.CourtAvailabilityUncheckedCreateNestedManyWithoutCourtInput
|
||||
bookings?: Prisma.CourtBookingUncheckedCreateNestedManyWithoutCourtInput
|
||||
maintenances?: Prisma.CourtMaintenanceUncheckedCreateNestedManyWithoutCourtInput
|
||||
recurringGroups?: Prisma.RecurringBookingGroupUncheckedCreateNestedManyWithoutCourtInput
|
||||
}
|
||||
|
||||
export type CourtCreateOrConnectWithoutPriceRulesInput = {
|
||||
@@ -1022,6 +1057,7 @@ export type CourtUpdateWithoutPriceRulesInput = {
|
||||
availabilities?: Prisma.CourtAvailabilityUpdateManyWithoutCourtNestedInput
|
||||
bookings?: Prisma.CourtBookingUpdateManyWithoutCourtNestedInput
|
||||
maintenances?: Prisma.CourtMaintenanceUpdateManyWithoutCourtNestedInput
|
||||
recurringGroups?: Prisma.RecurringBookingGroupUpdateManyWithoutCourtNestedInput
|
||||
}
|
||||
|
||||
export type CourtUncheckedUpdateWithoutPriceRulesInput = {
|
||||
@@ -1038,6 +1074,7 @@ export type CourtUncheckedUpdateWithoutPriceRulesInput = {
|
||||
availabilities?: Prisma.CourtAvailabilityUncheckedUpdateManyWithoutCourtNestedInput
|
||||
bookings?: Prisma.CourtBookingUncheckedUpdateManyWithoutCourtNestedInput
|
||||
maintenances?: Prisma.CourtMaintenanceUncheckedUpdateManyWithoutCourtNestedInput
|
||||
recurringGroups?: Prisma.RecurringBookingGroupUncheckedUpdateManyWithoutCourtNestedInput
|
||||
}
|
||||
|
||||
export type CourtCreateWithoutBookingsInput = {
|
||||
@@ -1054,6 +1091,7 @@ export type CourtCreateWithoutBookingsInput = {
|
||||
availabilities?: Prisma.CourtAvailabilityCreateNestedManyWithoutCourtInput
|
||||
priceRules?: Prisma.CourtPriceRuleCreateNestedManyWithoutCourtInput
|
||||
maintenances?: Prisma.CourtMaintenanceCreateNestedManyWithoutCourtInput
|
||||
recurringGroups?: Prisma.RecurringBookingGroupCreateNestedManyWithoutCourtInput
|
||||
}
|
||||
|
||||
export type CourtUncheckedCreateWithoutBookingsInput = {
|
||||
@@ -1070,6 +1108,7 @@ export type CourtUncheckedCreateWithoutBookingsInput = {
|
||||
availabilities?: Prisma.CourtAvailabilityUncheckedCreateNestedManyWithoutCourtInput
|
||||
priceRules?: Prisma.CourtPriceRuleUncheckedCreateNestedManyWithoutCourtInput
|
||||
maintenances?: Prisma.CourtMaintenanceUncheckedCreateNestedManyWithoutCourtInput
|
||||
recurringGroups?: Prisma.RecurringBookingGroupUncheckedCreateNestedManyWithoutCourtInput
|
||||
}
|
||||
|
||||
export type CourtCreateOrConnectWithoutBookingsInput = {
|
||||
@@ -1102,6 +1141,7 @@ export type CourtUpdateWithoutBookingsInput = {
|
||||
availabilities?: Prisma.CourtAvailabilityUpdateManyWithoutCourtNestedInput
|
||||
priceRules?: Prisma.CourtPriceRuleUpdateManyWithoutCourtNestedInput
|
||||
maintenances?: Prisma.CourtMaintenanceUpdateManyWithoutCourtNestedInput
|
||||
recurringGroups?: Prisma.RecurringBookingGroupUpdateManyWithoutCourtNestedInput
|
||||
}
|
||||
|
||||
export type CourtUncheckedUpdateWithoutBookingsInput = {
|
||||
@@ -1118,6 +1158,91 @@ export type CourtUncheckedUpdateWithoutBookingsInput = {
|
||||
availabilities?: Prisma.CourtAvailabilityUncheckedUpdateManyWithoutCourtNestedInput
|
||||
priceRules?: Prisma.CourtPriceRuleUncheckedUpdateManyWithoutCourtNestedInput
|
||||
maintenances?: Prisma.CourtMaintenanceUncheckedUpdateManyWithoutCourtNestedInput
|
||||
recurringGroups?: Prisma.RecurringBookingGroupUncheckedUpdateManyWithoutCourtNestedInput
|
||||
}
|
||||
|
||||
export type CourtCreateWithoutRecurringGroupsInput = {
|
||||
id: string
|
||||
name: string
|
||||
slotDurationMinutes: number
|
||||
basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string
|
||||
isUnderMaintenance?: boolean
|
||||
maintenanceReason?: string | null
|
||||
createdAt?: Date | string
|
||||
updatedAt?: Date | string
|
||||
complex: Prisma.ComplexCreateNestedOneWithoutCourtsInput
|
||||
sport: Prisma.SportCreateNestedOneWithoutCourtsInput
|
||||
availabilities?: Prisma.CourtAvailabilityCreateNestedManyWithoutCourtInput
|
||||
priceRules?: Prisma.CourtPriceRuleCreateNestedManyWithoutCourtInput
|
||||
bookings?: Prisma.CourtBookingCreateNestedManyWithoutCourtInput
|
||||
maintenances?: Prisma.CourtMaintenanceCreateNestedManyWithoutCourtInput
|
||||
}
|
||||
|
||||
export type CourtUncheckedCreateWithoutRecurringGroupsInput = {
|
||||
id: string
|
||||
complexId: string
|
||||
sportId: string
|
||||
name: string
|
||||
slotDurationMinutes: number
|
||||
basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string
|
||||
isUnderMaintenance?: boolean
|
||||
maintenanceReason?: string | null
|
||||
createdAt?: Date | string
|
||||
updatedAt?: Date | string
|
||||
availabilities?: Prisma.CourtAvailabilityUncheckedCreateNestedManyWithoutCourtInput
|
||||
priceRules?: Prisma.CourtPriceRuleUncheckedCreateNestedManyWithoutCourtInput
|
||||
bookings?: Prisma.CourtBookingUncheckedCreateNestedManyWithoutCourtInput
|
||||
maintenances?: Prisma.CourtMaintenanceUncheckedCreateNestedManyWithoutCourtInput
|
||||
}
|
||||
|
||||
export type CourtCreateOrConnectWithoutRecurringGroupsInput = {
|
||||
where: Prisma.CourtWhereUniqueInput
|
||||
create: Prisma.XOR<Prisma.CourtCreateWithoutRecurringGroupsInput, Prisma.CourtUncheckedCreateWithoutRecurringGroupsInput>
|
||||
}
|
||||
|
||||
export type CourtUpsertWithoutRecurringGroupsInput = {
|
||||
update: Prisma.XOR<Prisma.CourtUpdateWithoutRecurringGroupsInput, Prisma.CourtUncheckedUpdateWithoutRecurringGroupsInput>
|
||||
create: Prisma.XOR<Prisma.CourtCreateWithoutRecurringGroupsInput, Prisma.CourtUncheckedCreateWithoutRecurringGroupsInput>
|
||||
where?: Prisma.CourtWhereInput
|
||||
}
|
||||
|
||||
export type CourtUpdateToOneWithWhereWithoutRecurringGroupsInput = {
|
||||
where?: Prisma.CourtWhereInput
|
||||
data: Prisma.XOR<Prisma.CourtUpdateWithoutRecurringGroupsInput, Prisma.CourtUncheckedUpdateWithoutRecurringGroupsInput>
|
||||
}
|
||||
|
||||
export type CourtUpdateWithoutRecurringGroupsInput = {
|
||||
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string
|
||||
isUnderMaintenance?: Prisma.BoolFieldUpdateOperationsInput | boolean
|
||||
maintenanceReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
complex?: Prisma.ComplexUpdateOneRequiredWithoutCourtsNestedInput
|
||||
sport?: Prisma.SportUpdateOneRequiredWithoutCourtsNestedInput
|
||||
availabilities?: Prisma.CourtAvailabilityUpdateManyWithoutCourtNestedInput
|
||||
priceRules?: Prisma.CourtPriceRuleUpdateManyWithoutCourtNestedInput
|
||||
bookings?: Prisma.CourtBookingUpdateManyWithoutCourtNestedInput
|
||||
maintenances?: Prisma.CourtMaintenanceUpdateManyWithoutCourtNestedInput
|
||||
}
|
||||
|
||||
export type CourtUncheckedUpdateWithoutRecurringGroupsInput = {
|
||||
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
complexId?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
sportId?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string
|
||||
isUnderMaintenance?: Prisma.BoolFieldUpdateOperationsInput | boolean
|
||||
maintenanceReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
availabilities?: Prisma.CourtAvailabilityUncheckedUpdateManyWithoutCourtNestedInput
|
||||
priceRules?: Prisma.CourtPriceRuleUncheckedUpdateManyWithoutCourtNestedInput
|
||||
bookings?: Prisma.CourtBookingUncheckedUpdateManyWithoutCourtNestedInput
|
||||
maintenances?: Prisma.CourtMaintenanceUncheckedUpdateManyWithoutCourtNestedInput
|
||||
}
|
||||
|
||||
export type CourtCreateManyComplexInput = {
|
||||
@@ -1146,6 +1271,7 @@ export type CourtUpdateWithoutComplexInput = {
|
||||
priceRules?: Prisma.CourtPriceRuleUpdateManyWithoutCourtNestedInput
|
||||
bookings?: Prisma.CourtBookingUpdateManyWithoutCourtNestedInput
|
||||
maintenances?: Prisma.CourtMaintenanceUpdateManyWithoutCourtNestedInput
|
||||
recurringGroups?: Prisma.RecurringBookingGroupUpdateManyWithoutCourtNestedInput
|
||||
}
|
||||
|
||||
export type CourtUncheckedUpdateWithoutComplexInput = {
|
||||
@@ -1162,6 +1288,7 @@ export type CourtUncheckedUpdateWithoutComplexInput = {
|
||||
priceRules?: Prisma.CourtPriceRuleUncheckedUpdateManyWithoutCourtNestedInput
|
||||
bookings?: Prisma.CourtBookingUncheckedUpdateManyWithoutCourtNestedInput
|
||||
maintenances?: Prisma.CourtMaintenanceUncheckedUpdateManyWithoutCourtNestedInput
|
||||
recurringGroups?: Prisma.RecurringBookingGroupUncheckedUpdateManyWithoutCourtNestedInput
|
||||
}
|
||||
|
||||
export type CourtUncheckedUpdateManyWithoutComplexInput = {
|
||||
@@ -1202,6 +1329,7 @@ export type CourtUpdateWithoutSportInput = {
|
||||
priceRules?: Prisma.CourtPriceRuleUpdateManyWithoutCourtNestedInput
|
||||
bookings?: Prisma.CourtBookingUpdateManyWithoutCourtNestedInput
|
||||
maintenances?: Prisma.CourtMaintenanceUpdateManyWithoutCourtNestedInput
|
||||
recurringGroups?: Prisma.RecurringBookingGroupUpdateManyWithoutCourtNestedInput
|
||||
}
|
||||
|
||||
export type CourtUncheckedUpdateWithoutSportInput = {
|
||||
@@ -1218,6 +1346,7 @@ export type CourtUncheckedUpdateWithoutSportInput = {
|
||||
priceRules?: Prisma.CourtPriceRuleUncheckedUpdateManyWithoutCourtNestedInput
|
||||
bookings?: Prisma.CourtBookingUncheckedUpdateManyWithoutCourtNestedInput
|
||||
maintenances?: Prisma.CourtMaintenanceUncheckedUpdateManyWithoutCourtNestedInput
|
||||
recurringGroups?: Prisma.RecurringBookingGroupUncheckedUpdateManyWithoutCourtNestedInput
|
||||
}
|
||||
|
||||
export type CourtUncheckedUpdateManyWithoutSportInput = {
|
||||
@@ -1242,6 +1371,7 @@ export type CourtCountOutputType = {
|
||||
priceRules: number
|
||||
bookings: number
|
||||
maintenances: number
|
||||
recurringGroups: number
|
||||
}
|
||||
|
||||
export type CourtCountOutputTypeSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||
@@ -1249,6 +1379,7 @@ export type CourtCountOutputTypeSelect<ExtArgs extends runtime.Types.Extensions.
|
||||
priceRules?: boolean | CourtCountOutputTypeCountPriceRulesArgs
|
||||
bookings?: boolean | CourtCountOutputTypeCountBookingsArgs
|
||||
maintenances?: boolean | CourtCountOutputTypeCountMaintenancesArgs
|
||||
recurringGroups?: boolean | CourtCountOutputTypeCountRecurringGroupsArgs
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1289,6 +1420,13 @@ export type CourtCountOutputTypeCountMaintenancesArgs<ExtArgs extends runtime.Ty
|
||||
where?: Prisma.CourtMaintenanceWhereInput
|
||||
}
|
||||
|
||||
/**
|
||||
* CourtCountOutputType without action
|
||||
*/
|
||||
export type CourtCountOutputTypeCountRecurringGroupsArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||
where?: Prisma.RecurringBookingGroupWhereInput
|
||||
}
|
||||
|
||||
|
||||
export type CourtSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
|
||||
id?: boolean
|
||||
@@ -1307,6 +1445,7 @@ export type CourtSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs =
|
||||
priceRules?: boolean | Prisma.Court$priceRulesArgs<ExtArgs>
|
||||
bookings?: boolean | Prisma.Court$bookingsArgs<ExtArgs>
|
||||
maintenances?: boolean | Prisma.Court$maintenancesArgs<ExtArgs>
|
||||
recurringGroups?: boolean | Prisma.Court$recurringGroupsArgs<ExtArgs>
|
||||
_count?: boolean | Prisma.CourtCountOutputTypeDefaultArgs<ExtArgs>
|
||||
}, ExtArgs["result"]["court"]>
|
||||
|
||||
@@ -1361,6 +1500,7 @@ export type CourtInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs =
|
||||
priceRules?: boolean | Prisma.Court$priceRulesArgs<ExtArgs>
|
||||
bookings?: boolean | Prisma.Court$bookingsArgs<ExtArgs>
|
||||
maintenances?: boolean | Prisma.Court$maintenancesArgs<ExtArgs>
|
||||
recurringGroups?: boolean | Prisma.Court$recurringGroupsArgs<ExtArgs>
|
||||
_count?: boolean | Prisma.CourtCountOutputTypeDefaultArgs<ExtArgs>
|
||||
}
|
||||
export type CourtIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||
@@ -1381,6 +1521,7 @@ export type $CourtPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs
|
||||
priceRules: Prisma.$CourtPriceRulePayload<ExtArgs>[]
|
||||
bookings: Prisma.$CourtBookingPayload<ExtArgs>[]
|
||||
maintenances: Prisma.$CourtMaintenancePayload<ExtArgs>[]
|
||||
recurringGroups: Prisma.$RecurringBookingGroupPayload<ExtArgs>[]
|
||||
}
|
||||
scalars: runtime.Types.Extensions.GetPayloadResult<{
|
||||
id: string
|
||||
@@ -1793,6 +1934,7 @@ export interface Prisma__CourtClient<T, Null = never, ExtArgs extends runtime.Ty
|
||||
priceRules<T extends Prisma.Court$priceRulesArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Court$priceRulesArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$CourtPriceRulePayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
||||
bookings<T extends Prisma.Court$bookingsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Court$bookingsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$CourtBookingPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
||||
maintenances<T extends Prisma.Court$maintenancesArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Court$maintenancesArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$CourtMaintenancePayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
||||
recurringGroups<T extends Prisma.Court$recurringGroupsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Court$recurringGroupsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$RecurringBookingGroupPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
||||
/**
|
||||
* Attaches callbacks for the resolution and/or rejection of the Promise.
|
||||
* @param onfulfilled The callback to execute when the Promise is resolved.
|
||||
@@ -2328,6 +2470,30 @@ export type Court$maintenancesArgs<ExtArgs extends runtime.Types.Extensions.Inte
|
||||
distinct?: Prisma.CourtMaintenanceScalarFieldEnum | Prisma.CourtMaintenanceScalarFieldEnum[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Court.recurringGroups
|
||||
*/
|
||||
export type Court$recurringGroupsArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||
/**
|
||||
* Select specific fields to fetch from the RecurringBookingGroup
|
||||
*/
|
||||
select?: Prisma.RecurringBookingGroupSelect<ExtArgs> | null
|
||||
/**
|
||||
* Omit specific fields from the RecurringBookingGroup
|
||||
*/
|
||||
omit?: Prisma.RecurringBookingGroupOmit<ExtArgs> | null
|
||||
/**
|
||||
* Choose, which related nodes to fetch as well
|
||||
*/
|
||||
include?: Prisma.RecurringBookingGroupInclude<ExtArgs> | null
|
||||
where?: Prisma.RecurringBookingGroupWhereInput
|
||||
orderBy?: Prisma.RecurringBookingGroupOrderByWithRelationInput | Prisma.RecurringBookingGroupOrderByWithRelationInput[]
|
||||
cursor?: Prisma.RecurringBookingGroupWhereUniqueInput
|
||||
take?: number
|
||||
skip?: number
|
||||
distinct?: Prisma.RecurringBookingGroupScalarFieldEnum | Prisma.RecurringBookingGroupScalarFieldEnum[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Court without action
|
||||
*/
|
||||
|
||||
@@ -35,6 +35,7 @@ export type CourtBookingMinAggregateOutputType = {
|
||||
customerPhone: string | null
|
||||
customerEmail: string | null
|
||||
status: $Enums.CourtBookingStatus | null
|
||||
recurringGroupId: string | null
|
||||
createdAt: Date | null
|
||||
updatedAt: Date | null
|
||||
}
|
||||
@@ -50,6 +51,7 @@ export type CourtBookingMaxAggregateOutputType = {
|
||||
customerPhone: string | null
|
||||
customerEmail: string | null
|
||||
status: $Enums.CourtBookingStatus | null
|
||||
recurringGroupId: string | null
|
||||
createdAt: Date | null
|
||||
updatedAt: Date | null
|
||||
}
|
||||
@@ -65,6 +67,7 @@ export type CourtBookingCountAggregateOutputType = {
|
||||
customerPhone: number
|
||||
customerEmail: number
|
||||
status: number
|
||||
recurringGroupId: number
|
||||
createdAt: number
|
||||
updatedAt: number
|
||||
_all: number
|
||||
@@ -82,6 +85,7 @@ export type CourtBookingMinAggregateInputType = {
|
||||
customerPhone?: true
|
||||
customerEmail?: true
|
||||
status?: true
|
||||
recurringGroupId?: true
|
||||
createdAt?: true
|
||||
updatedAt?: true
|
||||
}
|
||||
@@ -97,6 +101,7 @@ export type CourtBookingMaxAggregateInputType = {
|
||||
customerPhone?: true
|
||||
customerEmail?: true
|
||||
status?: true
|
||||
recurringGroupId?: true
|
||||
createdAt?: true
|
||||
updatedAt?: true
|
||||
}
|
||||
@@ -112,6 +117,7 @@ export type CourtBookingCountAggregateInputType = {
|
||||
customerPhone?: true
|
||||
customerEmail?: true
|
||||
status?: true
|
||||
recurringGroupId?: true
|
||||
createdAt?: true
|
||||
updatedAt?: true
|
||||
_all?: true
|
||||
@@ -200,6 +206,7 @@ export type CourtBookingGroupByOutputType = {
|
||||
customerPhone: string
|
||||
customerEmail: string
|
||||
status: $Enums.CourtBookingStatus
|
||||
recurringGroupId: string | null
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
_count: CourtBookingCountAggregateOutputType | null
|
||||
@@ -236,9 +243,11 @@ export type CourtBookingWhereInput = {
|
||||
customerPhone?: Prisma.StringFilter<"CourtBooking"> | string
|
||||
customerEmail?: Prisma.StringFilter<"CourtBooking"> | string
|
||||
status?: Prisma.EnumCourtBookingStatusFilter<"CourtBooking"> | $Enums.CourtBookingStatus
|
||||
recurringGroupId?: Prisma.UuidNullableFilter<"CourtBooking"> | string | null
|
||||
createdAt?: Prisma.DateTimeFilter<"CourtBooking"> | Date | string
|
||||
updatedAt?: Prisma.DateTimeFilter<"CourtBooking"> | Date | string
|
||||
court?: Prisma.XOR<Prisma.CourtScalarRelationFilter, Prisma.CourtWhereInput>
|
||||
recurringGroup?: Prisma.XOR<Prisma.RecurringBookingGroupNullableScalarRelationFilter, Prisma.RecurringBookingGroupWhereInput> | null
|
||||
}
|
||||
|
||||
export type CourtBookingOrderByWithRelationInput = {
|
||||
@@ -252,9 +261,11 @@ export type CourtBookingOrderByWithRelationInput = {
|
||||
customerPhone?: Prisma.SortOrder
|
||||
customerEmail?: Prisma.SortOrder
|
||||
status?: Prisma.SortOrder
|
||||
recurringGroupId?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||
createdAt?: Prisma.SortOrder
|
||||
updatedAt?: Prisma.SortOrder
|
||||
court?: Prisma.CourtOrderByWithRelationInput
|
||||
recurringGroup?: Prisma.RecurringBookingGroupOrderByWithRelationInput
|
||||
}
|
||||
|
||||
export type CourtBookingWhereUniqueInput = Prisma.AtLeast<{
|
||||
@@ -272,9 +283,11 @@ export type CourtBookingWhereUniqueInput = Prisma.AtLeast<{
|
||||
customerPhone?: Prisma.StringFilter<"CourtBooking"> | string
|
||||
customerEmail?: Prisma.StringFilter<"CourtBooking"> | string
|
||||
status?: Prisma.EnumCourtBookingStatusFilter<"CourtBooking"> | $Enums.CourtBookingStatus
|
||||
recurringGroupId?: Prisma.UuidNullableFilter<"CourtBooking"> | string | null
|
||||
createdAt?: Prisma.DateTimeFilter<"CourtBooking"> | Date | string
|
||||
updatedAt?: Prisma.DateTimeFilter<"CourtBooking"> | Date | string
|
||||
court?: Prisma.XOR<Prisma.CourtScalarRelationFilter, Prisma.CourtWhereInput>
|
||||
recurringGroup?: Prisma.XOR<Prisma.RecurringBookingGroupNullableScalarRelationFilter, Prisma.RecurringBookingGroupWhereInput> | null
|
||||
}, "id" | "bookingCode" | "courtId_bookingDate_startTime">
|
||||
|
||||
export type CourtBookingOrderByWithAggregationInput = {
|
||||
@@ -288,6 +301,7 @@ export type CourtBookingOrderByWithAggregationInput = {
|
||||
customerPhone?: Prisma.SortOrder
|
||||
customerEmail?: Prisma.SortOrder
|
||||
status?: Prisma.SortOrder
|
||||
recurringGroupId?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||
createdAt?: Prisma.SortOrder
|
||||
updatedAt?: Prisma.SortOrder
|
||||
_count?: Prisma.CourtBookingCountOrderByAggregateInput
|
||||
@@ -309,6 +323,7 @@ export type CourtBookingScalarWhereWithAggregatesInput = {
|
||||
customerPhone?: Prisma.StringWithAggregatesFilter<"CourtBooking"> | string
|
||||
customerEmail?: Prisma.StringWithAggregatesFilter<"CourtBooking"> | string
|
||||
status?: Prisma.EnumCourtBookingStatusWithAggregatesFilter<"CourtBooking"> | $Enums.CourtBookingStatus
|
||||
recurringGroupId?: Prisma.UuidNullableWithAggregatesFilter<"CourtBooking"> | string | null
|
||||
createdAt?: Prisma.DateTimeWithAggregatesFilter<"CourtBooking"> | Date | string
|
||||
updatedAt?: Prisma.DateTimeWithAggregatesFilter<"CourtBooking"> | Date | string
|
||||
}
|
||||
@@ -326,6 +341,7 @@ export type CourtBookingCreateInput = {
|
||||
createdAt?: Date | string
|
||||
updatedAt?: Date | string
|
||||
court: Prisma.CourtCreateNestedOneWithoutBookingsInput
|
||||
recurringGroup?: Prisma.RecurringBookingGroupCreateNestedOneWithoutBookingsInput
|
||||
}
|
||||
|
||||
export type CourtBookingUncheckedCreateInput = {
|
||||
@@ -339,6 +355,7 @@ export type CourtBookingUncheckedCreateInput = {
|
||||
customerPhone: string
|
||||
customerEmail: string
|
||||
status?: $Enums.CourtBookingStatus
|
||||
recurringGroupId?: string | null
|
||||
createdAt?: Date | string
|
||||
updatedAt?: Date | string
|
||||
}
|
||||
@@ -356,6 +373,7 @@ export type CourtBookingUpdateInput = {
|
||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
court?: Prisma.CourtUpdateOneRequiredWithoutBookingsNestedInput
|
||||
recurringGroup?: Prisma.RecurringBookingGroupUpdateOneWithoutBookingsNestedInput
|
||||
}
|
||||
|
||||
export type CourtBookingUncheckedUpdateInput = {
|
||||
@@ -369,6 +387,7 @@ export type CourtBookingUncheckedUpdateInput = {
|
||||
customerPhone?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
customerEmail?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
status?: Prisma.EnumCourtBookingStatusFieldUpdateOperationsInput | $Enums.CourtBookingStatus
|
||||
recurringGroupId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
}
|
||||
@@ -384,6 +403,7 @@ export type CourtBookingCreateManyInput = {
|
||||
customerPhone: string
|
||||
customerEmail: string
|
||||
status?: $Enums.CourtBookingStatus
|
||||
recurringGroupId?: string | null
|
||||
createdAt?: Date | string
|
||||
updatedAt?: Date | string
|
||||
}
|
||||
@@ -413,6 +433,7 @@ export type CourtBookingUncheckedUpdateManyInput = {
|
||||
customerPhone?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
customerEmail?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
status?: Prisma.EnumCourtBookingStatusFieldUpdateOperationsInput | $Enums.CourtBookingStatus
|
||||
recurringGroupId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
}
|
||||
@@ -444,6 +465,7 @@ export type CourtBookingCountOrderByAggregateInput = {
|
||||
customerPhone?: Prisma.SortOrder
|
||||
customerEmail?: Prisma.SortOrder
|
||||
status?: Prisma.SortOrder
|
||||
recurringGroupId?: Prisma.SortOrder
|
||||
createdAt?: Prisma.SortOrder
|
||||
updatedAt?: Prisma.SortOrder
|
||||
}
|
||||
@@ -459,6 +481,7 @@ export type CourtBookingMaxOrderByAggregateInput = {
|
||||
customerPhone?: Prisma.SortOrder
|
||||
customerEmail?: Prisma.SortOrder
|
||||
status?: Prisma.SortOrder
|
||||
recurringGroupId?: Prisma.SortOrder
|
||||
createdAt?: Prisma.SortOrder
|
||||
updatedAt?: Prisma.SortOrder
|
||||
}
|
||||
@@ -474,6 +497,7 @@ export type CourtBookingMinOrderByAggregateInput = {
|
||||
customerPhone?: Prisma.SortOrder
|
||||
customerEmail?: Prisma.SortOrder
|
||||
status?: Prisma.SortOrder
|
||||
recurringGroupId?: Prisma.SortOrder
|
||||
createdAt?: Prisma.SortOrder
|
||||
updatedAt?: Prisma.SortOrder
|
||||
}
|
||||
@@ -524,6 +548,48 @@ export type EnumCourtBookingStatusFieldUpdateOperationsInput = {
|
||||
set?: $Enums.CourtBookingStatus
|
||||
}
|
||||
|
||||
export type CourtBookingCreateNestedManyWithoutRecurringGroupInput = {
|
||||
create?: Prisma.XOR<Prisma.CourtBookingCreateWithoutRecurringGroupInput, Prisma.CourtBookingUncheckedCreateWithoutRecurringGroupInput> | Prisma.CourtBookingCreateWithoutRecurringGroupInput[] | Prisma.CourtBookingUncheckedCreateWithoutRecurringGroupInput[]
|
||||
connectOrCreate?: Prisma.CourtBookingCreateOrConnectWithoutRecurringGroupInput | Prisma.CourtBookingCreateOrConnectWithoutRecurringGroupInput[]
|
||||
createMany?: Prisma.CourtBookingCreateManyRecurringGroupInputEnvelope
|
||||
connect?: Prisma.CourtBookingWhereUniqueInput | Prisma.CourtBookingWhereUniqueInput[]
|
||||
}
|
||||
|
||||
export type CourtBookingUncheckedCreateNestedManyWithoutRecurringGroupInput = {
|
||||
create?: Prisma.XOR<Prisma.CourtBookingCreateWithoutRecurringGroupInput, Prisma.CourtBookingUncheckedCreateWithoutRecurringGroupInput> | Prisma.CourtBookingCreateWithoutRecurringGroupInput[] | Prisma.CourtBookingUncheckedCreateWithoutRecurringGroupInput[]
|
||||
connectOrCreate?: Prisma.CourtBookingCreateOrConnectWithoutRecurringGroupInput | Prisma.CourtBookingCreateOrConnectWithoutRecurringGroupInput[]
|
||||
createMany?: Prisma.CourtBookingCreateManyRecurringGroupInputEnvelope
|
||||
connect?: Prisma.CourtBookingWhereUniqueInput | Prisma.CourtBookingWhereUniqueInput[]
|
||||
}
|
||||
|
||||
export type CourtBookingUpdateManyWithoutRecurringGroupNestedInput = {
|
||||
create?: Prisma.XOR<Prisma.CourtBookingCreateWithoutRecurringGroupInput, Prisma.CourtBookingUncheckedCreateWithoutRecurringGroupInput> | Prisma.CourtBookingCreateWithoutRecurringGroupInput[] | Prisma.CourtBookingUncheckedCreateWithoutRecurringGroupInput[]
|
||||
connectOrCreate?: Prisma.CourtBookingCreateOrConnectWithoutRecurringGroupInput | Prisma.CourtBookingCreateOrConnectWithoutRecurringGroupInput[]
|
||||
upsert?: Prisma.CourtBookingUpsertWithWhereUniqueWithoutRecurringGroupInput | Prisma.CourtBookingUpsertWithWhereUniqueWithoutRecurringGroupInput[]
|
||||
createMany?: Prisma.CourtBookingCreateManyRecurringGroupInputEnvelope
|
||||
set?: Prisma.CourtBookingWhereUniqueInput | Prisma.CourtBookingWhereUniqueInput[]
|
||||
disconnect?: Prisma.CourtBookingWhereUniqueInput | Prisma.CourtBookingWhereUniqueInput[]
|
||||
delete?: Prisma.CourtBookingWhereUniqueInput | Prisma.CourtBookingWhereUniqueInput[]
|
||||
connect?: Prisma.CourtBookingWhereUniqueInput | Prisma.CourtBookingWhereUniqueInput[]
|
||||
update?: Prisma.CourtBookingUpdateWithWhereUniqueWithoutRecurringGroupInput | Prisma.CourtBookingUpdateWithWhereUniqueWithoutRecurringGroupInput[]
|
||||
updateMany?: Prisma.CourtBookingUpdateManyWithWhereWithoutRecurringGroupInput | Prisma.CourtBookingUpdateManyWithWhereWithoutRecurringGroupInput[]
|
||||
deleteMany?: Prisma.CourtBookingScalarWhereInput | Prisma.CourtBookingScalarWhereInput[]
|
||||
}
|
||||
|
||||
export type CourtBookingUncheckedUpdateManyWithoutRecurringGroupNestedInput = {
|
||||
create?: Prisma.XOR<Prisma.CourtBookingCreateWithoutRecurringGroupInput, Prisma.CourtBookingUncheckedCreateWithoutRecurringGroupInput> | Prisma.CourtBookingCreateWithoutRecurringGroupInput[] | Prisma.CourtBookingUncheckedCreateWithoutRecurringGroupInput[]
|
||||
connectOrCreate?: Prisma.CourtBookingCreateOrConnectWithoutRecurringGroupInput | Prisma.CourtBookingCreateOrConnectWithoutRecurringGroupInput[]
|
||||
upsert?: Prisma.CourtBookingUpsertWithWhereUniqueWithoutRecurringGroupInput | Prisma.CourtBookingUpsertWithWhereUniqueWithoutRecurringGroupInput[]
|
||||
createMany?: Prisma.CourtBookingCreateManyRecurringGroupInputEnvelope
|
||||
set?: Prisma.CourtBookingWhereUniqueInput | Prisma.CourtBookingWhereUniqueInput[]
|
||||
disconnect?: Prisma.CourtBookingWhereUniqueInput | Prisma.CourtBookingWhereUniqueInput[]
|
||||
delete?: Prisma.CourtBookingWhereUniqueInput | Prisma.CourtBookingWhereUniqueInput[]
|
||||
connect?: Prisma.CourtBookingWhereUniqueInput | Prisma.CourtBookingWhereUniqueInput[]
|
||||
update?: Prisma.CourtBookingUpdateWithWhereUniqueWithoutRecurringGroupInput | Prisma.CourtBookingUpdateWithWhereUniqueWithoutRecurringGroupInput[]
|
||||
updateMany?: Prisma.CourtBookingUpdateManyWithWhereWithoutRecurringGroupInput | Prisma.CourtBookingUpdateManyWithWhereWithoutRecurringGroupInput[]
|
||||
deleteMany?: Prisma.CourtBookingScalarWhereInput | Prisma.CourtBookingScalarWhereInput[]
|
||||
}
|
||||
|
||||
export type CourtBookingCreateWithoutCourtInput = {
|
||||
id: string
|
||||
bookingCode: string
|
||||
@@ -536,6 +602,7 @@ export type CourtBookingCreateWithoutCourtInput = {
|
||||
status?: $Enums.CourtBookingStatus
|
||||
createdAt?: Date | string
|
||||
updatedAt?: Date | string
|
||||
recurringGroup?: Prisma.RecurringBookingGroupCreateNestedOneWithoutBookingsInput
|
||||
}
|
||||
|
||||
export type CourtBookingUncheckedCreateWithoutCourtInput = {
|
||||
@@ -548,6 +615,7 @@ export type CourtBookingUncheckedCreateWithoutCourtInput = {
|
||||
customerPhone: string
|
||||
customerEmail: string
|
||||
status?: $Enums.CourtBookingStatus
|
||||
recurringGroupId?: string | null
|
||||
createdAt?: Date | string
|
||||
updatedAt?: Date | string
|
||||
}
|
||||
@@ -592,10 +660,67 @@ export type CourtBookingScalarWhereInput = {
|
||||
customerPhone?: Prisma.StringFilter<"CourtBooking"> | string
|
||||
customerEmail?: Prisma.StringFilter<"CourtBooking"> | string
|
||||
status?: Prisma.EnumCourtBookingStatusFilter<"CourtBooking"> | $Enums.CourtBookingStatus
|
||||
recurringGroupId?: Prisma.UuidNullableFilter<"CourtBooking"> | string | null
|
||||
createdAt?: Prisma.DateTimeFilter<"CourtBooking"> | Date | string
|
||||
updatedAt?: Prisma.DateTimeFilter<"CourtBooking"> | Date | string
|
||||
}
|
||||
|
||||
export type CourtBookingCreateWithoutRecurringGroupInput = {
|
||||
id: string
|
||||
bookingCode: string
|
||||
bookingDate: Date | string
|
||||
startTime: string
|
||||
endTime: string
|
||||
customerName: string
|
||||
customerPhone: string
|
||||
customerEmail: string
|
||||
status?: $Enums.CourtBookingStatus
|
||||
createdAt?: Date | string
|
||||
updatedAt?: Date | string
|
||||
court: Prisma.CourtCreateNestedOneWithoutBookingsInput
|
||||
}
|
||||
|
||||
export type CourtBookingUncheckedCreateWithoutRecurringGroupInput = {
|
||||
id: string
|
||||
bookingCode: string
|
||||
courtId: string
|
||||
bookingDate: Date | string
|
||||
startTime: string
|
||||
endTime: string
|
||||
customerName: string
|
||||
customerPhone: string
|
||||
customerEmail: string
|
||||
status?: $Enums.CourtBookingStatus
|
||||
createdAt?: Date | string
|
||||
updatedAt?: Date | string
|
||||
}
|
||||
|
||||
export type CourtBookingCreateOrConnectWithoutRecurringGroupInput = {
|
||||
where: Prisma.CourtBookingWhereUniqueInput
|
||||
create: Prisma.XOR<Prisma.CourtBookingCreateWithoutRecurringGroupInput, Prisma.CourtBookingUncheckedCreateWithoutRecurringGroupInput>
|
||||
}
|
||||
|
||||
export type CourtBookingCreateManyRecurringGroupInputEnvelope = {
|
||||
data: Prisma.CourtBookingCreateManyRecurringGroupInput | Prisma.CourtBookingCreateManyRecurringGroupInput[]
|
||||
skipDuplicates?: boolean
|
||||
}
|
||||
|
||||
export type CourtBookingUpsertWithWhereUniqueWithoutRecurringGroupInput = {
|
||||
where: Prisma.CourtBookingWhereUniqueInput
|
||||
update: Prisma.XOR<Prisma.CourtBookingUpdateWithoutRecurringGroupInput, Prisma.CourtBookingUncheckedUpdateWithoutRecurringGroupInput>
|
||||
create: Prisma.XOR<Prisma.CourtBookingCreateWithoutRecurringGroupInput, Prisma.CourtBookingUncheckedCreateWithoutRecurringGroupInput>
|
||||
}
|
||||
|
||||
export type CourtBookingUpdateWithWhereUniqueWithoutRecurringGroupInput = {
|
||||
where: Prisma.CourtBookingWhereUniqueInput
|
||||
data: Prisma.XOR<Prisma.CourtBookingUpdateWithoutRecurringGroupInput, Prisma.CourtBookingUncheckedUpdateWithoutRecurringGroupInput>
|
||||
}
|
||||
|
||||
export type CourtBookingUpdateManyWithWhereWithoutRecurringGroupInput = {
|
||||
where: Prisma.CourtBookingScalarWhereInput
|
||||
data: Prisma.XOR<Prisma.CourtBookingUpdateManyMutationInput, Prisma.CourtBookingUncheckedUpdateManyWithoutRecurringGroupInput>
|
||||
}
|
||||
|
||||
export type CourtBookingCreateManyCourtInput = {
|
||||
id: string
|
||||
bookingCode: string
|
||||
@@ -606,6 +731,7 @@ export type CourtBookingCreateManyCourtInput = {
|
||||
customerPhone: string
|
||||
customerEmail: string
|
||||
status?: $Enums.CourtBookingStatus
|
||||
recurringGroupId?: string | null
|
||||
createdAt?: Date | string
|
||||
updatedAt?: Date | string
|
||||
}
|
||||
@@ -622,6 +748,7 @@ export type CourtBookingUpdateWithoutCourtInput = {
|
||||
status?: Prisma.EnumCourtBookingStatusFieldUpdateOperationsInput | $Enums.CourtBookingStatus
|
||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
recurringGroup?: Prisma.RecurringBookingGroupUpdateOneWithoutBookingsNestedInput
|
||||
}
|
||||
|
||||
export type CourtBookingUncheckedUpdateWithoutCourtInput = {
|
||||
@@ -634,6 +761,7 @@ export type CourtBookingUncheckedUpdateWithoutCourtInput = {
|
||||
customerPhone?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
customerEmail?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
status?: Prisma.EnumCourtBookingStatusFieldUpdateOperationsInput | $Enums.CourtBookingStatus
|
||||
recurringGroupId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
}
|
||||
@@ -648,6 +776,67 @@ export type CourtBookingUncheckedUpdateManyWithoutCourtInput = {
|
||||
customerPhone?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
customerEmail?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
status?: Prisma.EnumCourtBookingStatusFieldUpdateOperationsInput | $Enums.CourtBookingStatus
|
||||
recurringGroupId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
}
|
||||
|
||||
export type CourtBookingCreateManyRecurringGroupInput = {
|
||||
id: string
|
||||
bookingCode: string
|
||||
courtId: string
|
||||
bookingDate: Date | string
|
||||
startTime: string
|
||||
endTime: string
|
||||
customerName: string
|
||||
customerPhone: string
|
||||
customerEmail: string
|
||||
status?: $Enums.CourtBookingStatus
|
||||
createdAt?: Date | string
|
||||
updatedAt?: Date | string
|
||||
}
|
||||
|
||||
export type CourtBookingUpdateWithoutRecurringGroupInput = {
|
||||
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
bookingCode?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
bookingDate?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
startTime?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
endTime?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
customerName?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
customerPhone?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
customerEmail?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
status?: Prisma.EnumCourtBookingStatusFieldUpdateOperationsInput | $Enums.CourtBookingStatus
|
||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
court?: Prisma.CourtUpdateOneRequiredWithoutBookingsNestedInput
|
||||
}
|
||||
|
||||
export type CourtBookingUncheckedUpdateWithoutRecurringGroupInput = {
|
||||
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
bookingCode?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
courtId?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
bookingDate?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
startTime?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
endTime?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
customerName?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
customerPhone?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
customerEmail?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
status?: Prisma.EnumCourtBookingStatusFieldUpdateOperationsInput | $Enums.CourtBookingStatus
|
||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
}
|
||||
|
||||
export type CourtBookingUncheckedUpdateManyWithoutRecurringGroupInput = {
|
||||
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
bookingCode?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
courtId?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
bookingDate?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
startTime?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
endTime?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
customerName?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
customerPhone?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
customerEmail?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
status?: Prisma.EnumCourtBookingStatusFieldUpdateOperationsInput | $Enums.CourtBookingStatus
|
||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
}
|
||||
@@ -665,9 +854,11 @@ export type CourtBookingSelect<ExtArgs extends runtime.Types.Extensions.Internal
|
||||
customerPhone?: boolean
|
||||
customerEmail?: boolean
|
||||
status?: boolean
|
||||
recurringGroupId?: boolean
|
||||
createdAt?: boolean
|
||||
updatedAt?: boolean
|
||||
court?: boolean | Prisma.CourtDefaultArgs<ExtArgs>
|
||||
recurringGroup?: boolean | Prisma.CourtBooking$recurringGroupArgs<ExtArgs>
|
||||
}, ExtArgs["result"]["courtBooking"]>
|
||||
|
||||
export type CourtBookingSelectCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
|
||||
@@ -681,9 +872,11 @@ export type CourtBookingSelectCreateManyAndReturn<ExtArgs extends runtime.Types.
|
||||
customerPhone?: boolean
|
||||
customerEmail?: boolean
|
||||
status?: boolean
|
||||
recurringGroupId?: boolean
|
||||
createdAt?: boolean
|
||||
updatedAt?: boolean
|
||||
court?: boolean | Prisma.CourtDefaultArgs<ExtArgs>
|
||||
recurringGroup?: boolean | Prisma.CourtBooking$recurringGroupArgs<ExtArgs>
|
||||
}, ExtArgs["result"]["courtBooking"]>
|
||||
|
||||
export type CourtBookingSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
|
||||
@@ -697,9 +890,11 @@ export type CourtBookingSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.
|
||||
customerPhone?: boolean
|
||||
customerEmail?: boolean
|
||||
status?: boolean
|
||||
recurringGroupId?: boolean
|
||||
createdAt?: boolean
|
||||
updatedAt?: boolean
|
||||
court?: boolean | Prisma.CourtDefaultArgs<ExtArgs>
|
||||
recurringGroup?: boolean | Prisma.CourtBooking$recurringGroupArgs<ExtArgs>
|
||||
}, ExtArgs["result"]["courtBooking"]>
|
||||
|
||||
export type CourtBookingSelectScalar = {
|
||||
@@ -713,25 +908,30 @@ export type CourtBookingSelectScalar = {
|
||||
customerPhone?: boolean
|
||||
customerEmail?: boolean
|
||||
status?: boolean
|
||||
recurringGroupId?: boolean
|
||||
createdAt?: 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" | "customerEmail" | "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" | "recurringGroupId" | "createdAt" | "updatedAt", ExtArgs["result"]["courtBooking"]>
|
||||
export type CourtBookingInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||
court?: boolean | Prisma.CourtDefaultArgs<ExtArgs>
|
||||
recurringGroup?: boolean | Prisma.CourtBooking$recurringGroupArgs<ExtArgs>
|
||||
}
|
||||
export type CourtBookingIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||
court?: boolean | Prisma.CourtDefaultArgs<ExtArgs>
|
||||
recurringGroup?: boolean | Prisma.CourtBooking$recurringGroupArgs<ExtArgs>
|
||||
}
|
||||
export type CourtBookingIncludeUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||
court?: boolean | Prisma.CourtDefaultArgs<ExtArgs>
|
||||
recurringGroup?: boolean | Prisma.CourtBooking$recurringGroupArgs<ExtArgs>
|
||||
}
|
||||
|
||||
export type $CourtBookingPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||
name: "CourtBooking"
|
||||
objects: {
|
||||
court: Prisma.$CourtPayload<ExtArgs>
|
||||
recurringGroup: Prisma.$RecurringBookingGroupPayload<ExtArgs> | null
|
||||
}
|
||||
scalars: runtime.Types.Extensions.GetPayloadResult<{
|
||||
id: string
|
||||
@@ -744,6 +944,7 @@ export type $CourtBookingPayload<ExtArgs extends runtime.Types.Extensions.Intern
|
||||
customerPhone: string
|
||||
customerEmail: string
|
||||
status: $Enums.CourtBookingStatus
|
||||
recurringGroupId: string | null
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
}, ExtArgs["result"]["courtBooking"]>
|
||||
@@ -1141,6 +1342,7 @@ readonly fields: CourtBookingFieldRefs;
|
||||
export interface Prisma__CourtBookingClient<T, Null = never, ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs, GlobalOmitOptions = {}> extends Prisma.PrismaPromise<T> {
|
||||
readonly [Symbol.toStringTag]: "PrismaPromise"
|
||||
court<T extends Prisma.CourtDefaultArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.CourtDefaultArgs<ExtArgs>>): Prisma.Prisma__CourtClient<runtime.Types.Result.GetResult<Prisma.$CourtPayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | Null, Null, ExtArgs, GlobalOmitOptions>
|
||||
recurringGroup<T extends Prisma.CourtBooking$recurringGroupArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.CourtBooking$recurringGroupArgs<ExtArgs>>): Prisma.Prisma__RecurringBookingGroupClient<runtime.Types.Result.GetResult<Prisma.$RecurringBookingGroupPayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions>
|
||||
/**
|
||||
* Attaches callbacks for the resolution and/or rejection of the Promise.
|
||||
* @param onfulfilled The callback to execute when the Promise is resolved.
|
||||
@@ -1180,6 +1382,7 @@ export interface CourtBookingFieldRefs {
|
||||
readonly customerPhone: Prisma.FieldRef<"CourtBooking", 'String'>
|
||||
readonly customerEmail: Prisma.FieldRef<"CourtBooking", 'String'>
|
||||
readonly status: Prisma.FieldRef<"CourtBooking", 'CourtBookingStatus'>
|
||||
readonly recurringGroupId: Prisma.FieldRef<"CourtBooking", 'String'>
|
||||
readonly createdAt: Prisma.FieldRef<"CourtBooking", 'DateTime'>
|
||||
readonly updatedAt: Prisma.FieldRef<"CourtBooking", 'DateTime'>
|
||||
}
|
||||
@@ -1582,6 +1785,25 @@ export type CourtBookingDeleteManyArgs<ExtArgs extends runtime.Types.Extensions.
|
||||
limit?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* CourtBooking.recurringGroup
|
||||
*/
|
||||
export type CourtBooking$recurringGroupArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||
/**
|
||||
* Select specific fields to fetch from the RecurringBookingGroup
|
||||
*/
|
||||
select?: Prisma.RecurringBookingGroupSelect<ExtArgs> | null
|
||||
/**
|
||||
* Omit specific fields from the RecurringBookingGroup
|
||||
*/
|
||||
omit?: Prisma.RecurringBookingGroupOmit<ExtArgs> | null
|
||||
/**
|
||||
* Choose, which related nodes to fetch as well
|
||||
*/
|
||||
include?: Prisma.RecurringBookingGroupInclude<ExtArgs> | null
|
||||
where?: Prisma.RecurringBookingGroupWhereInput
|
||||
}
|
||||
|
||||
/**
|
||||
* CourtBooking without action
|
||||
*/
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
import { requireAuth } from '@/middlewares/require-auth.middleware';
|
||||
import { cancelRecurringGroupHandler } from '@/modules/admin-booking/handlers/cancel-recurring-group.handler';
|
||||
import { createAdminBookingHandler } from '@/modules/admin-booking/handlers/create-admin-booking.handler';
|
||||
import { createAdminRecurringBookingHandler } from '@/modules/admin-booking/handlers/create-admin-recurring-booking.handler';
|
||||
import { listAdminBookingsHandler } from '@/modules/admin-booking/handlers/list-admin-bookings.handler';
|
||||
import { listRecurringGroupsHandler } from '@/modules/admin-booking/handlers/list-recurring-groups.handler';
|
||||
import { rescheduleAdminBookingHandler } from '@/modules/admin-booking/handlers/reschedule-admin-booking.handler';
|
||||
import { updateAdminBookingStatusHandler } from '@/modules/admin-booking/handlers/update-admin-booking-status.handler';
|
||||
import { updateRecurringGroupHandler } from '@/modules/admin-booking/handlers/update-recurring-group.handler';
|
||||
import type { AppEnv } from '@/types/hono';
|
||||
import { zValidator } from '@hono/zod-validator';
|
||||
import {
|
||||
createAdminBookingSchema,
|
||||
createRecurringBookingSchema,
|
||||
listAdminBookingsQuerySchema,
|
||||
rescheduleAdminBookingSchema,
|
||||
updateAdminBookingStatusSchema,
|
||||
updateRecurringGroupSchema,
|
||||
} from '@repo/api-contract';
|
||||
import { Hono } from 'hono';
|
||||
import { z } from 'zod';
|
||||
@@ -15,6 +23,7 @@ import { z } from 'zod';
|
||||
export const adminBookingRoutes = new Hono<AppEnv>();
|
||||
const complexIdParamsSchema = z.object({ complexId: z.uuid() });
|
||||
const bookingIdParamsSchema = z.object({ id: z.uuid() });
|
||||
const groupIdParamsSchema = z.object({ groupId: z.uuid() });
|
||||
|
||||
adminBookingRoutes.use('*', requireAuth);
|
||||
|
||||
@@ -32,9 +41,42 @@ adminBookingRoutes.post(
|
||||
createAdminBookingHandler
|
||||
);
|
||||
|
||||
adminBookingRoutes.post(
|
||||
'/complex/:complexId/recurring',
|
||||
zValidator('param', complexIdParamsSchema),
|
||||
zValidator('json', createRecurringBookingSchema),
|
||||
createAdminRecurringBookingHandler
|
||||
);
|
||||
|
||||
adminBookingRoutes.post(
|
||||
'/recurring/:groupId/cancel',
|
||||
zValidator('param', groupIdParamsSchema),
|
||||
cancelRecurringGroupHandler
|
||||
);
|
||||
|
||||
adminBookingRoutes.get(
|
||||
'/complex/:complexId/recurring',
|
||||
zValidator('param', complexIdParamsSchema),
|
||||
listRecurringGroupsHandler
|
||||
);
|
||||
|
||||
adminBookingRoutes.patch(
|
||||
'/recurring/:groupId',
|
||||
zValidator('param', groupIdParamsSchema),
|
||||
zValidator('json', updateRecurringGroupSchema),
|
||||
updateRecurringGroupHandler
|
||||
);
|
||||
|
||||
adminBookingRoutes.patch(
|
||||
'/:id/status',
|
||||
zValidator('param', bookingIdParamsSchema),
|
||||
zValidator('json', updateAdminBookingStatusSchema),
|
||||
updateAdminBookingStatusHandler
|
||||
);
|
||||
|
||||
adminBookingRoutes.patch(
|
||||
'/:id/reschedule',
|
||||
zValidator('param', bookingIdParamsSchema),
|
||||
zValidator('json', rescheduleAdminBookingSchema),
|
||||
rescheduleAdminBookingHandler
|
||||
);
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { AdminBookingServiceError } from '@/modules/admin-booking/services/admin-booking.service';
|
||||
import { cancelRecurringGroup } from '@/modules/admin-booking/services/admin-recurring-booking.service';
|
||||
import type { AppContext } from '@/types/hono';
|
||||
|
||||
type GroupIdParams = { groupId: string };
|
||||
|
||||
export async function cancelRecurringGroupHandler(c: AppContext) {
|
||||
const { groupId } = c.req.valid('param' as never) as GroupIdParams;
|
||||
const user = c.get('user');
|
||||
|
||||
try {
|
||||
const result = await cancelRecurringGroup(user.id, groupId);
|
||||
return c.json(result);
|
||||
} catch (error) {
|
||||
if (error instanceof AdminBookingServiceError) {
|
||||
return c.json({ message: error.message }, error.status);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { AdminBookingServiceError } from '@/modules/admin-booking/services/admin-booking.service';
|
||||
import { createAdminRecurringBooking } from '@/modules/admin-booking/services/admin-recurring-booking.service';
|
||||
import { sendBookingConfirmation } from '@/services/booking-email.service';
|
||||
import type { AppContext } from '@/types/hono';
|
||||
import type { CreateRecurringBookingInput } from '@repo/api-contract';
|
||||
|
||||
type ComplexIdParams = { complexId: string };
|
||||
|
||||
export async function createAdminRecurringBookingHandler(c: AppContext) {
|
||||
const { complexId } = c.req.valid('param' as never) as ComplexIdParams;
|
||||
const payload = c.req.valid('json' as never) as CreateRecurringBookingInput;
|
||||
const user = c.get('user');
|
||||
|
||||
try {
|
||||
const group = await createAdminRecurringBooking(user.id, complexId, payload);
|
||||
|
||||
const firstBooking = group.bookings[0];
|
||||
if (firstBooking?.customerEmail) {
|
||||
void sendBookingConfirmation({
|
||||
bookingCode: firstBooking.bookingCode,
|
||||
complexName: firstBooking.complexName,
|
||||
date: firstBooking.date,
|
||||
startTime: firstBooking.startTime,
|
||||
endTime: firstBooking.endTime,
|
||||
courtName: firstBooking.courtName,
|
||||
sportName: firstBooking.sport.name,
|
||||
customerName: firstBooking.customerName,
|
||||
customerEmail: firstBooking.customerEmail,
|
||||
price: firstBooking.price,
|
||||
});
|
||||
}
|
||||
|
||||
return c.json(group, 201);
|
||||
} catch (error) {
|
||||
if (error instanceof AdminBookingServiceError) {
|
||||
return c.json({ message: error.message }, error.status);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { AdminBookingServiceError } from '@/modules/admin-booking/services/admin-booking.service';
|
||||
import { listRecurringGroups } from '@/modules/admin-booking/services/admin-recurring-booking.service';
|
||||
import type { AppContext } from '@/types/hono';
|
||||
|
||||
type ComplexIdParams = { complexId: string };
|
||||
|
||||
export async function listRecurringGroupsHandler(c: AppContext) {
|
||||
const { complexId } = c.req.valid('param' as never) as ComplexIdParams;
|
||||
|
||||
try {
|
||||
const groups = await listRecurringGroups(complexId);
|
||||
return c.json({ groups });
|
||||
} catch (error) {
|
||||
if (error instanceof AdminBookingServiceError) {
|
||||
return c.json({ message: error.message }, error.status);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { db } from '@/lib/prisma';
|
||||
import { sseManager } from '@/lib/sse';
|
||||
import {
|
||||
AdminBookingServiceError,
|
||||
rescheduleAdminBooking,
|
||||
} from '@/modules/admin-booking/services/admin-booking.service';
|
||||
import { sendBookingRescheduled } from '@/services/booking-email.service';
|
||||
import type { AppContext } from '@/types/hono';
|
||||
import type { RescheduleAdminBookingInput } from '@repo/api-contract';
|
||||
|
||||
type BookingIdParams = { id: string };
|
||||
|
||||
export async function rescheduleAdminBookingHandler(c: AppContext) {
|
||||
const { id } = c.req.valid('param' as never) as BookingIdParams;
|
||||
const payload = c.req.valid('json' as never) as RescheduleAdminBookingInput;
|
||||
const user = c.get('user');
|
||||
|
||||
const previous = await db.courtBooking.findUnique({
|
||||
where: { id },
|
||||
select: {
|
||||
court: { select: { name: true } },
|
||||
startTime: true,
|
||||
endTime: true,
|
||||
customerEmail: true,
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
const booking = await rescheduleAdminBooking(user.id, id, payload);
|
||||
|
||||
if (previous) {
|
||||
void sendBookingRescheduled({
|
||||
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: previous.customerEmail,
|
||||
previousCourtName: previous.court.name,
|
||||
previousStartTime: previous.startTime,
|
||||
previousEndTime: previous.endTime,
|
||||
});
|
||||
}
|
||||
|
||||
sseManager.emit(
|
||||
`complex:${booking.complexId}`,
|
||||
JSON.stringify({ type: 'reschedule', booking })
|
||||
);
|
||||
|
||||
return c.json(booking);
|
||||
} catch (error) {
|
||||
if (error instanceof AdminBookingServiceError) {
|
||||
return c.json({ message: error.message }, error.status);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { AdminBookingServiceError } from '@/modules/admin-booking/services/admin-booking.service';
|
||||
import { updateRecurringGroup } from '@/modules/admin-booking/services/admin-recurring-booking.service';
|
||||
import type { AppContext } from '@/types/hono';
|
||||
import type { UpdateRecurringGroupInput } from '@repo/api-contract';
|
||||
|
||||
type GroupIdParams = { groupId: string };
|
||||
|
||||
export async function updateRecurringGroupHandler(c: AppContext) {
|
||||
const { groupId } = c.req.valid('param' as never) as GroupIdParams;
|
||||
const payload = c.req.valid('json' as never) as UpdateRecurringGroupInput;
|
||||
const user = c.get('user');
|
||||
|
||||
try {
|
||||
const group = await updateRecurringGroup(user.id, groupId, payload);
|
||||
return c.json(group);
|
||||
} catch (error) {
|
||||
if (error instanceof AdminBookingServiceError) {
|
||||
return c.json({ message: error.message }, error.status);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import type {
|
||||
AdminBooking,
|
||||
CreateAdminBookingInput,
|
||||
ListAdminBookingsQuery,
|
||||
RescheduleAdminBookingInput,
|
||||
UpdateAdminBookingStatusInput,
|
||||
} from '@repo/api-contract';
|
||||
import { v7 as uuidv7 } from 'uuid';
|
||||
@@ -202,6 +203,7 @@ function mapBookingResponse(booking: {
|
||||
customerPhone: string;
|
||||
customerEmail: string;
|
||||
status: 'CONFIRMED' | 'CANCELLED' | 'COMPLETED' | 'NOSHOW';
|
||||
recurringGroupId: string | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
court: {
|
||||
@@ -239,6 +241,7 @@ function mapBookingResponse(booking: {
|
||||
customerEmail: booking.customerEmail,
|
||||
price: booking.price ?? 0,
|
||||
status: booking.status,
|
||||
recurringGroupId: booking.recurringGroupId,
|
||||
createdAt: booking.createdAt.toISOString(),
|
||||
updatedAt: booking.updatedAt.toISOString(),
|
||||
};
|
||||
@@ -431,6 +434,7 @@ export async function createAdminBooking(
|
||||
customerPhone: input.customerPhone.trim(),
|
||||
customerEmail: input.customerEmail?.trim() ?? '',
|
||||
status: 'CONFIRMED',
|
||||
recurringGroupId: null,
|
||||
},
|
||||
include: {
|
||||
court: {
|
||||
@@ -595,3 +599,196 @@ export async function updateAdminBookingStatus(
|
||||
|
||||
return mapBookingResponse({ ...booking, status: input.status });
|
||||
}
|
||||
|
||||
export async function rescheduleAdminBooking(
|
||||
userId: string,
|
||||
bookingId: string,
|
||||
input: RescheduleAdminBookingInput
|
||||
): Promise<AdminBooking> {
|
||||
const booking = await db.courtBooking.findFirst({
|
||||
where: {
|
||||
id: bookingId,
|
||||
court: {
|
||||
complex: {
|
||||
users: {
|
||||
some: { userId },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
court: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
complexId: true,
|
||||
slotDurationMinutes: true,
|
||||
sport: {
|
||||
select: { id: true, name: true, slug: true },
|
||||
},
|
||||
complex: {
|
||||
select: { id: true, complexName: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!booking) {
|
||||
throw new AdminBookingServiceError('Reserva no encontrada.', 404);
|
||||
}
|
||||
|
||||
if (booking.status !== 'CONFIRMED') {
|
||||
throw new AdminBookingServiceError(
|
||||
'Solo se pueden reprogramar reservas en estado confirmada.',
|
||||
409
|
||||
);
|
||||
}
|
||||
|
||||
const targetCourtId = input.courtId ?? booking.courtId;
|
||||
const targetStartTime = input.startTime ?? booking.startTime;
|
||||
|
||||
if (targetCourtId === booking.courtId && targetStartTime === booking.startTime) {
|
||||
throw new AdminBookingServiceError(
|
||||
'Debe proporcionar al menos una cancha o un horario diferente.',
|
||||
400
|
||||
);
|
||||
}
|
||||
|
||||
const dayOfWeek = getDayOfWeek(booking.bookingDate);
|
||||
|
||||
const targetCourt = await db.court.findFirst({
|
||||
where: {
|
||||
id: targetCourtId,
|
||||
complexId: booking.court.complexId,
|
||||
},
|
||||
include: {
|
||||
availabilities: {
|
||||
where: { dayOfWeek },
|
||||
orderBy: { startTime: 'asc' },
|
||||
},
|
||||
priceRules: {
|
||||
where: { isActive: true },
|
||||
orderBy: [{ dayOfWeek: 'asc' }, { startTime: 'asc' }],
|
||||
},
|
||||
sport: {
|
||||
select: { id: true, name: true, slug: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!targetCourt) {
|
||||
throw new AdminBookingServiceError('La cancha seleccionada no existe en el complejo.', 404);
|
||||
}
|
||||
|
||||
if (targetCourt.sport.id !== booking.court.sport.id) {
|
||||
throw new AdminBookingServiceError(
|
||||
'La cancha seleccionada no es del mismo deporte que la reserva original.',
|
||||
409
|
||||
);
|
||||
}
|
||||
|
||||
if (targetCourt.isUnderMaintenance) {
|
||||
throw new AdminBookingServiceError(
|
||||
'La cancha seleccionada se encuentra en mantenimiento.',
|
||||
409
|
||||
);
|
||||
}
|
||||
|
||||
const validSlots = buildSlots(targetCourt.availabilities, targetCourt.slotDurationMinutes);
|
||||
const selectedEndMinutes = toMinutes(targetStartTime) + targetCourt.slotDurationMinutes;
|
||||
const selectedSlot = {
|
||||
startTime: targetStartTime,
|
||||
endTime: minutesToTime(selectedEndMinutes),
|
||||
};
|
||||
|
||||
const slotExists = validSlots.some(
|
||||
(slot) => slot.startTime === selectedSlot.startTime && slot.endTime === selectedSlot.endTime
|
||||
);
|
||||
|
||||
if (!slotExists) {
|
||||
throw new AdminBookingServiceError(
|
||||
'El horario seleccionado no está disponible para esa cancha.',
|
||||
409
|
||||
);
|
||||
}
|
||||
|
||||
if (isSlotInPast(booking.bookingDate, targetStartTime, targetCourt.slotDurationMinutes)) {
|
||||
throw new AdminBookingServiceError('No se pueden reprogramar reservas en el pasado.', 400);
|
||||
}
|
||||
|
||||
const overlappingBooking = await db.courtBooking.findFirst({
|
||||
where: {
|
||||
id: { not: bookingId },
|
||||
courtId: targetCourtId,
|
||||
bookingDate: booking.bookingDate,
|
||||
status: 'CONFIRMED',
|
||||
startTime: { lt: selectedSlot.endTime },
|
||||
endTime: { gt: selectedSlot.startTime },
|
||||
},
|
||||
});
|
||||
|
||||
if (overlappingBooking) {
|
||||
throw new AdminBookingServiceError(
|
||||
'El horario seleccionado ya fue reservado por otra reserva.',
|
||||
409
|
||||
);
|
||||
}
|
||||
|
||||
const newPrice = resolvePrice(
|
||||
targetCourt,
|
||||
dayOfWeek,
|
||||
selectedSlot.startTime,
|
||||
selectedSlot.endTime
|
||||
);
|
||||
|
||||
await db.$transaction(async (tx) => {
|
||||
await tx.courtBookingLog.create({
|
||||
data: {
|
||||
id: uuidv7(),
|
||||
bookingCode: booking.bookingCode,
|
||||
courtId: booking.court.id,
|
||||
bookingDate: booking.bookingDate,
|
||||
startTime: booking.startTime,
|
||||
endTime: booking.endTime,
|
||||
customerName: booking.customerName,
|
||||
customerPhone: booking.customerPhone,
|
||||
customerEmail: booking.customerEmail,
|
||||
previousStatus: booking.status,
|
||||
newStatus: booking.status,
|
||||
previousCourtId: booking.court.id,
|
||||
previousStartTime: booking.startTime,
|
||||
previousEndTime: booking.endTime,
|
||||
changedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
await tx.courtBooking.update({
|
||||
where: { id: booking.id },
|
||||
data: {
|
||||
courtId: targetCourtId,
|
||||
startTime: selectedSlot.startTime,
|
||||
endTime: selectedSlot.endTime,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
const updatedBooking = {
|
||||
...booking,
|
||||
startTime: selectedSlot.startTime,
|
||||
endTime: selectedSlot.endTime,
|
||||
price: newPrice,
|
||||
};
|
||||
|
||||
if (targetCourtId !== booking.courtId) {
|
||||
updatedBooking.court = {
|
||||
id: targetCourt.id,
|
||||
name: targetCourt.name,
|
||||
slotDurationMinutes: targetCourt.slotDurationMinutes,
|
||||
sport: targetCourt.sport,
|
||||
complex: booking.court.complex,
|
||||
} as typeof booking.court;
|
||||
}
|
||||
|
||||
return mapBookingResponse(updatedBooking);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,893 @@
|
||||
import { randomInt } from 'node:crypto';
|
||||
import { DayOfWeek as DayOfWeekEnum } from '@/generated/prisma/enums';
|
||||
import { db } from '@/lib/prisma';
|
||||
import { isSlotInPast } from '@/lib/slot-validator';
|
||||
import {
|
||||
evaluatePlanUsage,
|
||||
isFeatureEnabled,
|
||||
parsePlanRules,
|
||||
} from '@/modules/plan/services/plan-rules.service';
|
||||
import type {
|
||||
CreateRecurringBookingInput,
|
||||
RecurringBookingGroup,
|
||||
UpdateRecurringGroupInput,
|
||||
} from '@repo/api-contract';
|
||||
import { v7 as uuidv7 } from 'uuid';
|
||||
import { AdminBookingServiceError } from './admin-booking.service';
|
||||
|
||||
const BOOKING_CODE_ALPHABET = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
|
||||
const BOOKING_CODE_LENGTH = 6;
|
||||
const MAX_RECURRING_WEEKS = 52;
|
||||
|
||||
const DAY_INDEX_BY_VALUE: Record<string, number> = {
|
||||
SUNDAY: 0,
|
||||
MONDAY: 1,
|
||||
TUESDAY: 2,
|
||||
WEDNESDAY: 3,
|
||||
THURSDAY: 4,
|
||||
FRIDAY: 5,
|
||||
SATURDAY: 6,
|
||||
};
|
||||
|
||||
const DAY_OF_WEEK_BY_INDEX = [
|
||||
DayOfWeekEnum.SUNDAY,
|
||||
DayOfWeekEnum.MONDAY,
|
||||
DayOfWeekEnum.TUESDAY,
|
||||
DayOfWeekEnum.WEDNESDAY,
|
||||
DayOfWeekEnum.THURSDAY,
|
||||
DayOfWeekEnum.FRIDAY,
|
||||
DayOfWeekEnum.SATURDAY,
|
||||
] as const;
|
||||
|
||||
type DayOfWeek = (typeof DAY_OF_WEEK_BY_INDEX)[number];
|
||||
|
||||
function toMinutes(value: string): number {
|
||||
const [hours, minutes] = value.split(':').map((part) => Number(part));
|
||||
return hours * 60 + minutes;
|
||||
}
|
||||
|
||||
function minutesToTime(minutes: number): string {
|
||||
const safeMinutes = Math.max(0, minutes);
|
||||
const hours = Math.floor(safeMinutes / 60);
|
||||
const mins = safeMinutes % 60;
|
||||
return `${String(hours).padStart(2, '0')}:${String(mins).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
function parseIsoDate(date: string): Date {
|
||||
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(date);
|
||||
|
||||
if (!match) {
|
||||
throw new AdminBookingServiceError('La fecha debe tener formato YYYY-MM-DD.', 400);
|
||||
}
|
||||
|
||||
const year = Number(match[1]);
|
||||
const month = Number(match[2]);
|
||||
const day = Number(match[3]);
|
||||
|
||||
const parsed = new Date(Date.UTC(year, month - 1, day));
|
||||
|
||||
if (
|
||||
Number.isNaN(parsed.getTime()) ||
|
||||
parsed.getUTCFullYear() !== year ||
|
||||
parsed.getUTCMonth() + 1 !== month ||
|
||||
parsed.getUTCDate() !== day
|
||||
) {
|
||||
throw new AdminBookingServiceError('La fecha enviada no es valida.', 400);
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function formatIsoDate(date: Date): string {
|
||||
return date.toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
function getDayOfWeekValue(date: Date): DayOfWeek {
|
||||
const dayOfWeek = DAY_OF_WEEK_BY_INDEX[date.getUTCDay()];
|
||||
|
||||
if (!dayOfWeek) {
|
||||
throw new AdminBookingServiceError('No se pudo resolver el dia de la semana.', 400);
|
||||
}
|
||||
|
||||
return dayOfWeek;
|
||||
}
|
||||
|
||||
function generateBookingCode(): string {
|
||||
let code = '';
|
||||
|
||||
for (let index = 0; index < BOOKING_CODE_LENGTH; index += 1) {
|
||||
code += BOOKING_CODE_ALPHABET[randomInt(0, BOOKING_CODE_ALPHABET.length)];
|
||||
}
|
||||
|
||||
return code;
|
||||
}
|
||||
|
||||
function buildSlots(
|
||||
availability: Array<{
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
}>,
|
||||
slotDurationMinutes: number
|
||||
) {
|
||||
const slots: Array<{ startTime: string; endTime: string }> = [];
|
||||
|
||||
for (const range of availability) {
|
||||
const start = toMinutes(range.startTime);
|
||||
const end = toMinutes(range.endTime);
|
||||
|
||||
for (
|
||||
let current = start;
|
||||
current + slotDurationMinutes <= end;
|
||||
current += slotDurationMinutes
|
||||
) {
|
||||
slots.push({
|
||||
startTime: minutesToTime(current),
|
||||
endTime: minutesToTime(current + slotDurationMinutes),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return slots;
|
||||
}
|
||||
|
||||
async function ensureComplexAccess(complexId: string, userId: string) {
|
||||
const complexUser = await db.complexUser.findUnique({
|
||||
where: {
|
||||
complexId_userId: {
|
||||
complexId,
|
||||
userId,
|
||||
},
|
||||
},
|
||||
include: {
|
||||
complex: {
|
||||
select: {
|
||||
id: true,
|
||||
complexName: true,
|
||||
plan: {
|
||||
select: {
|
||||
rules: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!complexUser) {
|
||||
throw new AdminBookingServiceError('No tienes permisos para administrar este complejo.', 403);
|
||||
}
|
||||
|
||||
return complexUser.complex;
|
||||
}
|
||||
|
||||
function* generateRecurringDates(
|
||||
startDate: Date,
|
||||
endDate: Date | null,
|
||||
dayOfWeek: number
|
||||
): Generator<Date> {
|
||||
const current = new Date(startDate);
|
||||
current.setUTCDate(current.getUTCDate() + ((dayOfWeek - current.getUTCDay() + 7) % 7));
|
||||
|
||||
if (current < startDate) {
|
||||
current.setUTCDate(current.getUTCDate() + 7);
|
||||
}
|
||||
|
||||
const maxDate = endDate ?? new Date(startDate);
|
||||
if (!endDate) {
|
||||
maxDate.setUTCDate(maxDate.getUTCDate() + MAX_RECURRING_WEEKS * 7);
|
||||
}
|
||||
|
||||
while (current <= maxDate) {
|
||||
yield new Date(current);
|
||||
current.setUTCDate(current.getUTCDate() + 7);
|
||||
}
|
||||
}
|
||||
|
||||
function mapBookingResponse(booking: {
|
||||
id: string;
|
||||
bookingCode: string;
|
||||
bookingDate: Date;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
customerName: string;
|
||||
customerPhone: string;
|
||||
customerEmail: string;
|
||||
status: 'CONFIRMED' | 'CANCELLED' | 'COMPLETED' | 'NOSHOW';
|
||||
recurringGroupId: string | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
court: {
|
||||
id: string;
|
||||
name: string;
|
||||
complex: {
|
||||
id: string;
|
||||
complexName: string;
|
||||
};
|
||||
sport: {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
};
|
||||
};
|
||||
price?: number;
|
||||
}) {
|
||||
return {
|
||||
id: booking.id,
|
||||
bookingCode: booking.bookingCode,
|
||||
complexId: booking.court.complex.id,
|
||||
complexName: booking.court.complex.complexName,
|
||||
courtId: booking.court.id,
|
||||
courtName: booking.court.name,
|
||||
sport: {
|
||||
id: booking.court.sport.id,
|
||||
name: booking.court.sport.name,
|
||||
slug: booking.court.sport.slug,
|
||||
},
|
||||
date: formatIsoDate(booking.bookingDate),
|
||||
startTime: booking.startTime,
|
||||
endTime: booking.endTime,
|
||||
customerName: booking.customerName,
|
||||
customerPhone: booking.customerPhone,
|
||||
customerEmail: booking.customerEmail,
|
||||
price: booking.price ?? 0,
|
||||
status: booking.status,
|
||||
recurringGroupId: booking.recurringGroupId,
|
||||
createdAt: booking.createdAt.toISOString(),
|
||||
updatedAt: booking.updatedAt.toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
export async function createAdminRecurringBooking(
|
||||
userId: string,
|
||||
complexId: string,
|
||||
input: CreateRecurringBookingInput
|
||||
): Promise<RecurringBookingGroup> {
|
||||
const complex = await ensureComplexAccess(complexId, userId);
|
||||
|
||||
const adminUser = await db.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: { emailVerified: true },
|
||||
});
|
||||
|
||||
if (!adminUser?.emailVerified) {
|
||||
throw new AdminBookingServiceError('Debés verificar tu email para poder crear reservas.', 403);
|
||||
}
|
||||
|
||||
if (!complex.plan) {
|
||||
throw new AdminBookingServiceError('El complejo no tiene un plan asignado.', 403);
|
||||
}
|
||||
|
||||
const rules = parsePlanRules(complex.plan.rules);
|
||||
|
||||
if (!isFeatureEnabled(rules, 'fixedSlots')) {
|
||||
throw new AdminBookingServiceError(
|
||||
'Tu plan no permite la creación de turnos fijos. Comunicate con el administrador.',
|
||||
403
|
||||
);
|
||||
}
|
||||
|
||||
const startDate = parseIsoDate(input.date);
|
||||
const dayOfWeek = getDayOfWeekValue(startDate);
|
||||
const endDate = input.recurringEndDate ? parseIsoDate(input.recurringEndDate) : null;
|
||||
|
||||
if (endDate && endDate <= startDate) {
|
||||
throw new AdminBookingServiceError(
|
||||
'La fecha de fin debe ser posterior a la fecha de inicio.',
|
||||
400
|
||||
);
|
||||
}
|
||||
|
||||
const court = await db.court.findFirst({
|
||||
where: {
|
||||
id: input.courtId,
|
||||
complexId,
|
||||
},
|
||||
include: {
|
||||
availabilities: {
|
||||
where: {
|
||||
dayOfWeek,
|
||||
},
|
||||
orderBy: {
|
||||
startTime: 'asc',
|
||||
},
|
||||
},
|
||||
sport: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
slug: true,
|
||||
},
|
||||
},
|
||||
priceRules: {
|
||||
where: { isActive: true },
|
||||
orderBy: [{ dayOfWeek: 'asc' }, { startTime: 'asc' }],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!court) {
|
||||
throw new AdminBookingServiceError('La cancha seleccionada no existe en el complejo.', 404);
|
||||
}
|
||||
|
||||
const validSlots = buildSlots(court.availabilities, court.slotDurationMinutes);
|
||||
const selectedEndMinutes = toMinutes(input.startTime) + court.slotDurationMinutes;
|
||||
const selectedSlot = {
|
||||
startTime: input.startTime,
|
||||
endTime: minutesToTime(selectedEndMinutes),
|
||||
};
|
||||
|
||||
const slotExists = validSlots.some(
|
||||
(slot) => slot.startTime === selectedSlot.startTime && slot.endTime === selectedSlot.endTime
|
||||
);
|
||||
|
||||
if (!slotExists) {
|
||||
throw new AdminBookingServiceError(
|
||||
'El horario seleccionado no esta disponible para esa cancha.',
|
||||
409
|
||||
);
|
||||
}
|
||||
|
||||
if (isSlotInPast(startDate, input.startTime, court.slotDurationMinutes)) {
|
||||
throw new AdminBookingServiceError('La fecha de inicio no puede estar en el pasado.', 400);
|
||||
}
|
||||
|
||||
const recurringDates = Array.from(
|
||||
generateRecurringDates(startDate, endDate, startDate.getUTCDay())
|
||||
);
|
||||
|
||||
if (recurringDates.length === 0) {
|
||||
throw new AdminBookingServiceError(
|
||||
'No se generaron fechas para la reserva periódica. Verifica las fechas ingresadas.',
|
||||
400
|
||||
);
|
||||
}
|
||||
|
||||
for (let attempt = 0; attempt < 5; attempt += 1) {
|
||||
try {
|
||||
const result = await db.$transaction(async (tx) => {
|
||||
const groupId = uuidv7();
|
||||
|
||||
const bookingsForDate = await tx.courtBooking.count({
|
||||
where: {
|
||||
startTime: selectedSlot.startTime,
|
||||
bookingDate: startDate,
|
||||
status: 'CONFIRMED',
|
||||
court: {
|
||||
complexId,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const courtsCount = await tx.court.count({
|
||||
where: { complexId },
|
||||
});
|
||||
|
||||
const violations = evaluatePlanUsage(rules, {
|
||||
courtsCount,
|
||||
bookingsToday: bookingsForDate,
|
||||
});
|
||||
|
||||
const maxBookingsViolation = violations.find(
|
||||
(v) => v.code === 'MAX_BOOKINGS_PER_DAY_REACHED'
|
||||
);
|
||||
|
||||
if (maxBookingsViolation) {
|
||||
throw new AdminBookingServiceError(maxBookingsViolation.message, 409);
|
||||
}
|
||||
|
||||
const group = await tx.recurringBookingGroup.create({
|
||||
data: {
|
||||
id: groupId,
|
||||
complexId,
|
||||
courtId: court.id,
|
||||
startTime: selectedSlot.startTime,
|
||||
endTime: selectedSlot.endTime,
|
||||
dayOfWeek,
|
||||
startDate,
|
||||
endDate,
|
||||
status: 'ACTIVE',
|
||||
customerName: input.customerName.trim(),
|
||||
customerPhone: input.customerPhone.trim(),
|
||||
customerEmail: input.customerEmail?.trim() ?? '',
|
||||
},
|
||||
});
|
||||
|
||||
const createdBookings = [];
|
||||
|
||||
for (const date of recurringDates) {
|
||||
if (isSlotInPast(date, input.startTime, court.slotDurationMinutes)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const overlappingBooking = await tx.courtBooking.findFirst({
|
||||
where: {
|
||||
courtId: court.id,
|
||||
bookingDate: date,
|
||||
status: 'CONFIRMED',
|
||||
startTime: {
|
||||
lt: selectedSlot.endTime,
|
||||
},
|
||||
endTime: {
|
||||
gt: selectedSlot.startTime,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (overlappingBooking) {
|
||||
throw new AdminBookingServiceError(
|
||||
`El horario seleccionado ya fue reservado para el dia ${formatIsoDate(date)}.`,
|
||||
409
|
||||
);
|
||||
}
|
||||
|
||||
const booking = await tx.courtBooking.create({
|
||||
data: {
|
||||
id: uuidv7(),
|
||||
bookingCode: generateBookingCode(),
|
||||
courtId: court.id,
|
||||
bookingDate: date,
|
||||
startTime: selectedSlot.startTime,
|
||||
endTime: selectedSlot.endTime,
|
||||
customerName: input.customerName.trim(),
|
||||
customerPhone: input.customerPhone.trim(),
|
||||
customerEmail: input.customerEmail?.trim() ?? '',
|
||||
status: 'CONFIRMED',
|
||||
recurringGroupId: groupId,
|
||||
},
|
||||
include: {
|
||||
court: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
sport: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
slug: true,
|
||||
},
|
||||
},
|
||||
complex: {
|
||||
select: {
|
||||
id: true,
|
||||
complexName: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
createdBookings.push(booking);
|
||||
}
|
||||
|
||||
return { group, bookings: createdBookings };
|
||||
});
|
||||
|
||||
return {
|
||||
id: result.group.id,
|
||||
complexId,
|
||||
courtId: court.id,
|
||||
startTime: result.group.startTime,
|
||||
endTime: result.group.endTime,
|
||||
dayOfWeek: result.group.dayOfWeek,
|
||||
startDate: formatIsoDate(result.group.startDate),
|
||||
endDate: result.group.endDate ? formatIsoDate(result.group.endDate) : null,
|
||||
status: 'ACTIVE' as const,
|
||||
customerName: result.group.customerName,
|
||||
customerPhone: result.group.customerPhone,
|
||||
customerEmail: result.group.customerEmail,
|
||||
bookings: result.bookings.map((b) => mapBookingResponse(b)),
|
||||
createdAt: result.group.createdAt.toISOString(),
|
||||
updatedAt: result.group.updatedAt.toISOString(),
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof AdminBookingServiceError) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const prismaError = error as {
|
||||
code?: string;
|
||||
meta?: {
|
||||
target?: string[] | string;
|
||||
};
|
||||
};
|
||||
|
||||
if (prismaError.code === 'P2002') {
|
||||
const targets = Array.isArray(prismaError.meta?.target)
|
||||
? prismaError.meta?.target
|
||||
: [prismaError.meta?.target];
|
||||
const isBookingCodeCollision = targets.some((target) =>
|
||||
String(target).includes('booking_code')
|
||||
);
|
||||
|
||||
if (isBookingCodeCollision) {
|
||||
continue;
|
||||
}
|
||||
|
||||
throw new AdminBookingServiceError('El horario seleccionado ya fue reservado.', 409);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
throw new AdminBookingServiceError(
|
||||
'No se pudo generar un codigo de reserva unico. Intenta nuevamente.',
|
||||
409
|
||||
);
|
||||
}
|
||||
|
||||
export async function cancelRecurringGroup(userId: string, groupId: string) {
|
||||
const group = await db.recurringBookingGroup.findUnique({
|
||||
where: { id: groupId },
|
||||
include: {
|
||||
complex: {
|
||||
select: {
|
||||
id: true,
|
||||
users: {
|
||||
where: { userId },
|
||||
select: { userId: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!group) {
|
||||
throw new AdminBookingServiceError('Grupo de reservas no encontrado.', 404);
|
||||
}
|
||||
|
||||
if (group.complex.users.length === 0) {
|
||||
throw new AdminBookingServiceError('No tienes permisos para administrar este complejo.', 403);
|
||||
}
|
||||
|
||||
if (group.status === 'CANCELLED') {
|
||||
throw new AdminBookingServiceError('El grupo ya fue cancelado anteriormente.', 409);
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const todayStart = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));
|
||||
|
||||
await db.$transaction(async (tx) => {
|
||||
await tx.recurringBookingGroup.update({
|
||||
where: { id: groupId },
|
||||
data: { status: 'CANCELLED' },
|
||||
});
|
||||
|
||||
const futureBookings = await tx.courtBooking.findMany({
|
||||
where: {
|
||||
recurringGroupId: groupId,
|
||||
bookingDate: { gte: todayStart },
|
||||
status: 'CONFIRMED',
|
||||
},
|
||||
});
|
||||
|
||||
for (const booking of futureBookings) {
|
||||
await tx.courtBookingLog.create({
|
||||
data: {
|
||||
id: uuidv7(),
|
||||
bookingCode: booking.bookingCode,
|
||||
courtId: booking.courtId,
|
||||
bookingDate: booking.bookingDate,
|
||||
startTime: booking.startTime,
|
||||
endTime: booking.endTime,
|
||||
customerName: booking.customerName,
|
||||
customerPhone: booking.customerPhone,
|
||||
customerEmail: booking.customerEmail,
|
||||
previousStatus: booking.status,
|
||||
newStatus: 'CANCELLED',
|
||||
changedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
await tx.courtBooking.delete({
|
||||
where: { id: booking.id },
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
export async function listRecurringGroups(complexId: string): Promise<RecurringBookingGroup[]> {
|
||||
const groups = await db.recurringBookingGroup.findMany({
|
||||
where: { complexId, status: 'ACTIVE' },
|
||||
include: {
|
||||
court: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
sport: {
|
||||
select: { id: true, name: true, slug: true },
|
||||
},
|
||||
complex: {
|
||||
select: { id: true, complexName: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
bookings: {
|
||||
orderBy: { bookingDate: 'asc' },
|
||||
include: {
|
||||
court: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
sport: {
|
||||
select: { id: true, name: true, slug: true },
|
||||
},
|
||||
complex: {
|
||||
select: { id: true, complexName: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
|
||||
return groups.map((group) => ({
|
||||
id: group.id,
|
||||
complexId: group.complexId,
|
||||
courtId: group.courtId,
|
||||
startTime: group.startTime,
|
||||
endTime: group.endTime,
|
||||
dayOfWeek: group.dayOfWeek,
|
||||
startDate: formatIsoDate(group.startDate),
|
||||
endDate: group.endDate ? formatIsoDate(group.endDate) : null,
|
||||
status: group.status,
|
||||
customerName: group.customerName,
|
||||
customerPhone: group.customerPhone,
|
||||
customerEmail: group.customerEmail,
|
||||
bookings: group.bookings.map(mapBookingResponse),
|
||||
createdAt: group.createdAt.toISOString(),
|
||||
updatedAt: group.updatedAt.toISOString(),
|
||||
}));
|
||||
}
|
||||
|
||||
export async function updateRecurringGroup(
|
||||
userId: string,
|
||||
groupId: string,
|
||||
input: UpdateRecurringGroupInput
|
||||
): Promise<RecurringBookingGroup> {
|
||||
const group = await db.recurringBookingGroup.findUnique({
|
||||
where: { id: groupId },
|
||||
include: {
|
||||
complex: {
|
||||
select: {
|
||||
id: true,
|
||||
users: {
|
||||
where: { userId },
|
||||
select: { userId: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!group) {
|
||||
throw new AdminBookingServiceError('Grupo de turnos fijos no encontrado.', 404);
|
||||
}
|
||||
|
||||
if (group.complex.users.length === 0) {
|
||||
throw new AdminBookingServiceError('No tienes permisos para administrar este complejo.', 403);
|
||||
}
|
||||
|
||||
if (group.status === 'CANCELLED') {
|
||||
throw new AdminBookingServiceError('No se puede editar un grupo cancelado.', 409);
|
||||
}
|
||||
|
||||
const courtId = input.courtId ?? group.courtId;
|
||||
const dayOfWeek = input.dayOfWeek ?? group.dayOfWeek;
|
||||
const startTime = input.startTime ?? group.startTime;
|
||||
|
||||
const scheduleChanged =
|
||||
input.courtId !== undefined || input.dayOfWeek !== undefined || input.startTime !== undefined;
|
||||
|
||||
if (scheduleChanged) {
|
||||
const court = await db.court.findFirst({
|
||||
where: { id: courtId, complexId: group.complexId },
|
||||
include: {
|
||||
availabilities: {
|
||||
where: { dayOfWeek: dayOfWeek as DayOfWeek },
|
||||
orderBy: { startTime: 'asc' },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!court) {
|
||||
throw new AdminBookingServiceError('La cancha seleccionada no existe en el complejo.', 404);
|
||||
}
|
||||
|
||||
const validSlots = buildSlots(court.availabilities, court.slotDurationMinutes);
|
||||
const selectedEndMinutes = toMinutes(startTime) + court.slotDurationMinutes;
|
||||
const endTime = minutesToTime(selectedEndMinutes);
|
||||
|
||||
const slotExists = validSlots.some(
|
||||
(slot) => slot.startTime === startTime && slot.endTime === endTime
|
||||
);
|
||||
|
||||
if (!slotExists) {
|
||||
throw new AdminBookingServiceError(
|
||||
'El horario seleccionado no esta disponible para esa cancha.',
|
||||
409
|
||||
);
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const todayStart = new Date(
|
||||
Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())
|
||||
);
|
||||
|
||||
await db.$transaction(async (tx) => {
|
||||
await tx.recurringBookingGroup.update({
|
||||
where: { id: groupId },
|
||||
data: {
|
||||
courtId,
|
||||
dayOfWeek: dayOfWeek as DayOfWeek,
|
||||
startTime,
|
||||
endTime,
|
||||
customerName: input.customerName?.trim() ?? group.customerName,
|
||||
customerPhone: input.customerPhone?.trim() ?? group.customerPhone,
|
||||
customerEmail: input.customerEmail?.trim() ?? group.customerEmail,
|
||||
},
|
||||
});
|
||||
|
||||
const futureBookings = await tx.courtBooking.findMany({
|
||||
where: {
|
||||
recurringGroupId: groupId,
|
||||
bookingDate: { gte: todayStart },
|
||||
status: 'CONFIRMED',
|
||||
},
|
||||
});
|
||||
|
||||
for (const booking of futureBookings) {
|
||||
await tx.courtBookingLog.create({
|
||||
data: {
|
||||
id: uuidv7(),
|
||||
bookingCode: booking.bookingCode,
|
||||
courtId: booking.courtId,
|
||||
bookingDate: booking.bookingDate,
|
||||
startTime: booking.startTime,
|
||||
endTime: booking.endTime,
|
||||
customerName: booking.customerName,
|
||||
customerPhone: booking.customerPhone,
|
||||
customerEmail: booking.customerEmail,
|
||||
previousStatus: booking.status,
|
||||
newStatus: 'CANCELLED',
|
||||
changedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
await tx.courtBooking.delete({
|
||||
where: { id: booking.id },
|
||||
});
|
||||
}
|
||||
|
||||
const dayIndex = DAY_INDEX_BY_VALUE[dayOfWeek] ?? 0;
|
||||
const recurringDates = Array.from(
|
||||
generateRecurringDates(todayStart, group.endDate, dayIndex)
|
||||
);
|
||||
|
||||
for (const date of recurringDates) {
|
||||
const overlappingBooking = await tx.courtBooking.findFirst({
|
||||
where: {
|
||||
courtId,
|
||||
bookingDate: date,
|
||||
status: 'CONFIRMED',
|
||||
startTime: { lt: endTime },
|
||||
endTime: { gt: startTime },
|
||||
},
|
||||
});
|
||||
|
||||
if (overlappingBooking) {
|
||||
throw new AdminBookingServiceError(
|
||||
`El horario seleccionado ya fue reservado para el dia ${formatIsoDate(date)}.`,
|
||||
409
|
||||
);
|
||||
}
|
||||
|
||||
await tx.courtBooking.create({
|
||||
data: {
|
||||
id: uuidv7(),
|
||||
bookingCode: generateBookingCode(),
|
||||
courtId,
|
||||
bookingDate: date,
|
||||
startTime,
|
||||
endTime,
|
||||
customerName: input.customerName?.trim() ?? group.customerName,
|
||||
customerPhone: input.customerPhone?.trim() ?? group.customerPhone,
|
||||
customerEmail: input.customerEmail?.trim() ?? group.customerEmail,
|
||||
status: 'CONFIRMED',
|
||||
recurringGroupId: groupId,
|
||||
},
|
||||
include: {
|
||||
court: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
sport: { select: { id: true, name: true, slug: true } },
|
||||
complex: { select: { id: true, complexName: true } },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
await db.recurringBookingGroup.update({
|
||||
where: { id: groupId },
|
||||
data: {
|
||||
...(input.customerName !== undefined && { customerName: input.customerName.trim() }),
|
||||
...(input.customerPhone !== undefined && { customerPhone: input.customerPhone.trim() }),
|
||||
...(input.customerEmail !== undefined && { customerEmail: input.customerEmail.trim() }),
|
||||
},
|
||||
});
|
||||
|
||||
const now = new Date();
|
||||
const todayStart = new Date(
|
||||
Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())
|
||||
);
|
||||
|
||||
const updateData: Record<string, string> = {};
|
||||
if (input.customerName !== undefined) updateData.customerName = input.customerName.trim();
|
||||
if (input.customerPhone !== undefined) updateData.customerPhone = input.customerPhone.trim();
|
||||
if (input.customerEmail !== undefined) updateData.customerEmail = input.customerEmail.trim();
|
||||
|
||||
if (Object.keys(updateData).length > 0) {
|
||||
await db.courtBooking.updateMany({
|
||||
where: { recurringGroupId: groupId, bookingDate: { gte: todayStart } },
|
||||
data: updateData,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const updated = await db.recurringBookingGroup.findUnique({
|
||||
where: { id: groupId },
|
||||
include: {
|
||||
court: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
sport: { select: { id: true, name: true, slug: true } },
|
||||
complex: { select: { id: true, complexName: true } },
|
||||
},
|
||||
},
|
||||
bookings: {
|
||||
orderBy: { bookingDate: 'asc' },
|
||||
include: {
|
||||
court: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
sport: { select: { id: true, name: true, slug: true } },
|
||||
complex: { select: { id: true, complexName: true } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!updated) {
|
||||
throw new AdminBookingServiceError('Error al actualizar el grupo.', 409);
|
||||
}
|
||||
|
||||
return {
|
||||
id: updated.id,
|
||||
complexId: updated.complexId,
|
||||
courtId: updated.courtId,
|
||||
startTime: updated.startTime,
|
||||
endTime: updated.endTime,
|
||||
dayOfWeek: updated.dayOfWeek,
|
||||
startDate: formatIsoDate(updated.startDate),
|
||||
endDate: updated.endDate ? formatIsoDate(updated.endDate) : null,
|
||||
status: updated.status,
|
||||
customerName: updated.customerName,
|
||||
customerPhone: updated.customerPhone,
|
||||
customerEmail: updated.customerEmail,
|
||||
bookings: updated.bookings.map(mapBookingResponse),
|
||||
createdAt: updated.createdAt.toISOString(),
|
||||
updatedAt: updated.updatedAt.toISOString(),
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Prisma } from '@/generated/prisma/client';
|
||||
import { db } from '@/lib/prisma';
|
||||
import { parsePlanRules } from '@/modules/plan/services/plan-rules.service';
|
||||
import { v7 as uuidv7 } from 'uuid';
|
||||
|
||||
export type CreateComplexInput = {
|
||||
@@ -185,7 +186,11 @@ export async function listMyComplexes(userId: string) {
|
||||
const complexUsers = await db.complexUser.findMany({
|
||||
where: { userId },
|
||||
include: {
|
||||
complex: true,
|
||||
complex: {
|
||||
include: {
|
||||
plan: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'asc',
|
||||
@@ -195,6 +200,9 @@ export async function listMyComplexes(userId: string) {
|
||||
return complexUsers.map((complexUser) => ({
|
||||
...complexUser.complex,
|
||||
role: complexUser.role,
|
||||
planFeatures: complexUser.complex.plan
|
||||
? parsePlanRules(complexUser.complex.plan.rules).features
|
||||
: null,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -207,7 +215,11 @@ export async function getCurrentComplex(userId: string, complexId: string) {
|
||||
},
|
||||
},
|
||||
include: {
|
||||
complex: true,
|
||||
complex: {
|
||||
include: {
|
||||
plan: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -216,6 +228,9 @@ export async function getCurrentComplex(userId: string, complexId: string) {
|
||||
return {
|
||||
...complexUser.complex,
|
||||
role: complexUser.role,
|
||||
planFeatures: complexUser.complex.plan
|
||||
? parsePlanRules(complexUser.complex.plan.rules).features
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -228,7 +243,11 @@ export async function selectComplex(userId: string, complexId: string) {
|
||||
},
|
||||
},
|
||||
include: {
|
||||
complex: true,
|
||||
complex: {
|
||||
include: {
|
||||
plan: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -237,6 +256,9 @@ export async function selectComplex(userId: string, complexId: string) {
|
||||
return {
|
||||
...complexUser.complex,
|
||||
role: complexUser.role,
|
||||
planFeatures: complexUser.complex.plan
|
||||
? parsePlanRules(complexUser.complex.plan.rules).features
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -167,6 +167,7 @@ function mapCourtResponse(court: CourtWithRelations) {
|
||||
id: court.id,
|
||||
complexId: court.complexId,
|
||||
name: court.name,
|
||||
isUnderMaintenance: court.isUnderMaintenance,
|
||||
sportId: court.sportId,
|
||||
sport: {
|
||||
id: court.sport.id,
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
bookingCancelledHtml,
|
||||
bookingConfirmationHtml,
|
||||
bookingNoShowHtml,
|
||||
bookingRescheduledHtml,
|
||||
} from '@/emails/booking-confirmation';
|
||||
import { sendMail } from '@/lib/mailer';
|
||||
|
||||
@@ -86,3 +87,35 @@ export async function sendBookingNoShow(data: BookingEmailInput) {
|
||||
text: `Reserva no concretada en ${data.complexName}. Codigo: ${data.bookingCode}. Fecha: ${data.date}. Horario: ${data.startTime} - ${data.endTime}.`,
|
||||
});
|
||||
}
|
||||
|
||||
type BookingRescheduledEmailInput = BookingEmailInput & {
|
||||
previousCourtName: string;
|
||||
previousStartTime: string;
|
||||
previousEndTime: string;
|
||||
};
|
||||
|
||||
export async function sendBookingRescheduled(data: BookingRescheduledEmailInput) {
|
||||
if (!data.customerEmail) return;
|
||||
|
||||
const html = bookingRescheduledHtml({
|
||||
bookingCode: data.bookingCode,
|
||||
complexSlug: data.complexSlug,
|
||||
complexName: data.complexName,
|
||||
date: data.date,
|
||||
startTime: data.startTime,
|
||||
endTime: data.endTime,
|
||||
courtName: data.courtName,
|
||||
sportName: data.sportName,
|
||||
customerName: data.customerName,
|
||||
previousCourtName: data.previousCourtName,
|
||||
previousStartTime: data.previousStartTime,
|
||||
previousEndTime: data.previousEndTime,
|
||||
});
|
||||
|
||||
await sendMail({
|
||||
to: data.customerEmail,
|
||||
subject: `Reserva reprogramada en ${data.complexName} | Playzer`,
|
||||
html,
|
||||
text: `Reserva reprogramada en ${data.complexName}. Codigo: ${data.bookingCode}. Nueva cancha: ${data.courtName}. Nuevo horario: ${data.startTime} - ${data.endTime}.`,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
import { beforeEach, expect, mock, test } from 'bun:test';
|
||||
|
||||
const rescheduleAdminBookingMock = mock(
|
||||
async (_userId: string, _id: string, _input: unknown) => ({})
|
||||
);
|
||||
|
||||
class MockAdminBookingServiceError extends Error {
|
||||
status: 400 | 403 | 404 | 409;
|
||||
|
||||
constructor(message: string, status: 400 | 403 | 404 | 409 = 400) {
|
||||
super(message);
|
||||
this.name = 'AdminBookingServiceError';
|
||||
this.status = status;
|
||||
}
|
||||
}
|
||||
|
||||
mock.module('@/modules/admin-booking/services/admin-booking.service', () => ({
|
||||
__esModule: true,
|
||||
AdminBookingServiceError: MockAdminBookingServiceError,
|
||||
rescheduleAdminBooking: rescheduleAdminBookingMock,
|
||||
}));
|
||||
|
||||
const { rescheduleAdminBookingHandler } = await import(
|
||||
'@/modules/admin-booking/handlers/reschedule-admin-booking.handler'
|
||||
);
|
||||
|
||||
type HandlerContext = {
|
||||
get: (key: 'user' | 'requestId') => { id: string } | undefined;
|
||||
req: {
|
||||
valid: (type: string) => Record<string, unknown>;
|
||||
};
|
||||
json: ReturnType<typeof mock>;
|
||||
};
|
||||
|
||||
function createContext(input: { userId: string; body: unknown }) {
|
||||
const json = mock((payload: unknown, init?: unknown) => ({
|
||||
payload,
|
||||
init,
|
||||
}));
|
||||
|
||||
const c = {
|
||||
get: (key: 'user' | 'requestId') => {
|
||||
if (key === 'user') return { id: input.userId };
|
||||
return undefined;
|
||||
},
|
||||
req: {
|
||||
valid: (type: string) => {
|
||||
if (type === 'param') return { id: 'booking-1' };
|
||||
if (type === 'json') return input.body as Record<string, unknown>;
|
||||
return {};
|
||||
},
|
||||
},
|
||||
json,
|
||||
} as unknown as HandlerContext;
|
||||
|
||||
return { c, json };
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
rescheduleAdminBookingMock.mockReset();
|
||||
});
|
||||
|
||||
test('returns 200 with the updated booking', async () => {
|
||||
const updatedBooking = {
|
||||
id: 'booking-1',
|
||||
bookingCode: 'ABC123',
|
||||
complexId: 'complex-1',
|
||||
complexName: 'Complejo Test',
|
||||
courtId: 'court-2',
|
||||
courtName: 'Cancha 2',
|
||||
sport: { id: 'sport-1', name: 'Tenis', slug: 'tenis' },
|
||||
date: '2026-07-15',
|
||||
startTime: '14:00',
|
||||
endTime: '15:00',
|
||||
customerName: 'Juan Perez',
|
||||
customerPhone: '1144444444',
|
||||
customerEmail: 'juan@example.com',
|
||||
price: 15000,
|
||||
status: 'CONFIRMED',
|
||||
recurringGroupId: null,
|
||||
createdAt: '2026-07-14T00:00:00.000Z',
|
||||
updatedAt: '2026-07-14T00:00:00.000Z',
|
||||
} as const;
|
||||
|
||||
rescheduleAdminBookingMock.mockResolvedValue(updatedBooking);
|
||||
|
||||
const { c, json } = createContext({
|
||||
userId: 'user-1',
|
||||
body: { courtId: 'court-2', startTime: '14:00' },
|
||||
});
|
||||
|
||||
const response = await rescheduleAdminBookingHandler(c as never);
|
||||
|
||||
expect(rescheduleAdminBookingMock).toHaveBeenCalledWith('user-1', 'booking-1', {
|
||||
courtId: 'court-2',
|
||||
startTime: '14:00',
|
||||
});
|
||||
expect(json.mock.calls[0]?.[0]).toEqual(updatedBooking);
|
||||
expect(json.mock.calls[0]?.[1]).toBeUndefined();
|
||||
expect(response).toBeDefined();
|
||||
});
|
||||
|
||||
test('maps AdminBookingServiceError to an error response', async () => {
|
||||
rescheduleAdminBookingMock.mockRejectedValue(
|
||||
new MockAdminBookingServiceError(
|
||||
'Solo se pueden reprogramar reservas en estado confirmada.',
|
||||
409
|
||||
)
|
||||
);
|
||||
|
||||
const { c, json } = createContext({
|
||||
userId: 'user-1',
|
||||
body: { startTime: '14:00' },
|
||||
});
|
||||
|
||||
const response = await rescheduleAdminBookingHandler(c as never);
|
||||
|
||||
expect(rescheduleAdminBookingMock).toHaveBeenCalledWith('user-1', 'booking-1', {
|
||||
startTime: '14:00',
|
||||
});
|
||||
expect(json.mock.calls[0]?.[0]).toEqual({
|
||||
message: 'Solo se pueden reprogramar reservas en estado confirmada.',
|
||||
});
|
||||
expect(json.mock.calls[0]?.[1]).toBe(409);
|
||||
expect(response).toBeDefined();
|
||||
});
|
||||
@@ -0,0 +1,317 @@
|
||||
import { beforeEach, expect, test } from 'bun:test';
|
||||
|
||||
import { prismaMock, transactionMock, uuidV7Mock } from '../support/prisma.mock';
|
||||
|
||||
const { AdminBookingServiceError, rescheduleAdminBooking } = await import(
|
||||
'@/modules/admin-booking/services/admin-booking.service'
|
||||
);
|
||||
|
||||
function makeBooking(overrides?: Record<string, unknown>) {
|
||||
return {
|
||||
id: 'booking-1',
|
||||
bookingCode: 'ABC123',
|
||||
courtId: 'court-1',
|
||||
bookingDate: new Date('2026-07-15T00:00:00.000Z'),
|
||||
startTime: '10:00',
|
||||
endTime: '11:00',
|
||||
customerName: 'Juan Perez',
|
||||
customerPhone: '1144444444',
|
||||
customerEmail: 'juan@example.com',
|
||||
status: 'CONFIRMED',
|
||||
recurringGroupId: null,
|
||||
createdAt: new Date('2026-07-14T00:00:00.000Z'),
|
||||
updatedAt: new Date('2026-07-14T00:00:00.000Z'),
|
||||
court: {
|
||||
id: 'court-1',
|
||||
name: 'Cancha 1',
|
||||
complexId: 'complex-1',
|
||||
slotDurationMinutes: 60,
|
||||
sport: { id: 'sport-1', name: 'Tenis', slug: 'tenis' },
|
||||
complex: { id: 'complex-1', complexName: 'Complejo Test' },
|
||||
},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
// July 15, 2026 is a Wednesday
|
||||
const WEDNESDAY = 'WEDNESDAY';
|
||||
|
||||
function makeCourt(overrides?: Record<string, unknown>) {
|
||||
return {
|
||||
id: 'court-2',
|
||||
name: 'Cancha 2',
|
||||
complexId: 'complex-1',
|
||||
slotDurationMinutes: 60,
|
||||
basePrice: 15000,
|
||||
isUnderMaintenance: false,
|
||||
availabilities: [{ id: 'avail-1', dayOfWeek: WEDNESDAY, startTime: '08:00', endTime: '18:00' }],
|
||||
priceRules: [],
|
||||
sport: { id: 'sport-1', name: 'Tenis', slug: 'tenis' },
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
prismaMock._reset();
|
||||
transactionMock.mockClear();
|
||||
uuidV7Mock.mockReset();
|
||||
uuidV7Mock.mockReturnValue('00000000-0000-4000-8000-000000000001');
|
||||
});
|
||||
|
||||
test('reschedules a booking changing both court and time', async () => {
|
||||
prismaMock.courtBooking.findFirst.mockResolvedValue(makeBooking() as never);
|
||||
prismaMock.court.findFirst.mockResolvedValue(makeCourt() as never);
|
||||
|
||||
prismaMock.courtBooking.findFirst
|
||||
.mockResolvedValueOnce(makeBooking() as never) // initial fetch
|
||||
.mockResolvedValueOnce(null as never); // overlap check -> no overlap
|
||||
|
||||
transactionMock.mockImplementation(async <T>(fn: (tx: typeof prismaMock) => Promise<T>) =>
|
||||
fn(prismaMock)
|
||||
);
|
||||
|
||||
prismaMock.courtBookingLog.create.mockResolvedValue({} as never);
|
||||
prismaMock.courtBooking.update.mockResolvedValue(
|
||||
makeBooking({
|
||||
courtId: 'court-2',
|
||||
startTime: '14:00',
|
||||
endTime: '15:00',
|
||||
}) as never
|
||||
);
|
||||
|
||||
const result = await rescheduleAdminBooking('user-1', 'booking-1', {
|
||||
courtId: 'court-2',
|
||||
startTime: '14:00',
|
||||
});
|
||||
|
||||
expect(result.courtId).toBe('court-2');
|
||||
expect(result.startTime).toBe('14:00');
|
||||
expect(result.courtName).toBe('Cancha 2');
|
||||
expect(prismaMock.courtBookingLog.create).toHaveBeenCalledTimes(1);
|
||||
expect(prismaMock.courtBooking.update).toHaveBeenCalledWith({
|
||||
where: { id: 'booking-1' },
|
||||
data: {
|
||||
courtId: 'court-2',
|
||||
startTime: '14:00',
|
||||
endTime: '15:00',
|
||||
},
|
||||
});
|
||||
|
||||
const logArgs = prismaMock.courtBookingLog.create.mock.calls[0]?.[0] as {
|
||||
data: { previousCourtId: string; previousStartTime: string; previousEndTime: string };
|
||||
};
|
||||
expect(logArgs.data.previousCourtId).toBe('court-1');
|
||||
expect(logArgs.data.previousStartTime).toBe('10:00');
|
||||
expect(logArgs.data.previousEndTime).toBe('11:00');
|
||||
});
|
||||
|
||||
test('reschedules a booking changing only the time', async () => {
|
||||
prismaMock.courtBooking.findFirst
|
||||
.mockResolvedValueOnce(makeBooking() as never) // initial fetch
|
||||
.mockResolvedValueOnce(null as never); // overlap check
|
||||
|
||||
// Same court, different time
|
||||
prismaMock.court.findFirst.mockResolvedValue(
|
||||
makeCourt({
|
||||
id: 'court-1',
|
||||
name: 'Cancha 1',
|
||||
availabilities: [
|
||||
{ id: 'avail-1', dayOfWeek: WEDNESDAY, startTime: '08:00', endTime: '18:00' },
|
||||
],
|
||||
}) as never
|
||||
);
|
||||
|
||||
transactionMock.mockImplementation(async <T>(fn: (tx: typeof prismaMock) => Promise<T>) =>
|
||||
fn(prismaMock)
|
||||
);
|
||||
|
||||
prismaMock.courtBookingLog.create.mockResolvedValue({} as never);
|
||||
prismaMock.courtBooking.update.mockResolvedValue(
|
||||
makeBooking({
|
||||
startTime: '15:00',
|
||||
endTime: '16:00',
|
||||
}) as never
|
||||
);
|
||||
|
||||
const result = await rescheduleAdminBooking('user-1', 'booking-1', {
|
||||
startTime: '15:00',
|
||||
});
|
||||
|
||||
expect(result.startTime).toBe('15:00');
|
||||
expect(result.courtId).toBe('court-1');
|
||||
expect(prismaMock.courtBookingLog.create).toHaveBeenCalledTimes(1);
|
||||
expect(prismaMock.courtBooking.update).toHaveBeenCalledWith({
|
||||
where: { id: 'booking-1' },
|
||||
data: {
|
||||
courtId: 'court-1',
|
||||
startTime: '15:00',
|
||||
endTime: '16:00',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('rejects when booking is not found', async () => {
|
||||
prismaMock.courtBooking.findFirst.mockResolvedValue(null as never);
|
||||
|
||||
let caught: unknown;
|
||||
try {
|
||||
await rescheduleAdminBooking('user-1', 'non-existent', { startTime: '14:00' });
|
||||
} catch (error) {
|
||||
caught = error;
|
||||
}
|
||||
|
||||
expect(caught).toBeInstanceOf(AdminBookingServiceError);
|
||||
expect((caught as InstanceType<typeof AdminBookingServiceError>).status).toBe(404);
|
||||
expect(transactionMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('rejects when booking status is not CONFIRMED', async () => {
|
||||
prismaMock.courtBooking.findFirst.mockResolvedValue(
|
||||
makeBooking({ status: 'COMPLETED' }) as never
|
||||
);
|
||||
|
||||
let caught: unknown;
|
||||
try {
|
||||
await rescheduleAdminBooking('user-1', 'booking-1', { startTime: '14:00' });
|
||||
} catch (error) {
|
||||
caught = error;
|
||||
}
|
||||
|
||||
expect(caught).toBeInstanceOf(AdminBookingServiceError);
|
||||
expect((caught as InstanceType<typeof AdminBookingServiceError>).status).toBe(409);
|
||||
expect(transactionMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('rejects when slot is not available in target court', async () => {
|
||||
prismaMock.courtBooking.findFirst.mockResolvedValue(makeBooking() as never);
|
||||
// Court with no availabilities that match the requested time
|
||||
prismaMock.court.findFirst.mockResolvedValue(
|
||||
makeCourt({
|
||||
id: 'court-2',
|
||||
availabilities: [
|
||||
{ id: 'avail-1', dayOfWeek: WEDNESDAY, startTime: '08:00', endTime: '09:00' },
|
||||
],
|
||||
}) as never
|
||||
);
|
||||
|
||||
let caught: unknown;
|
||||
try {
|
||||
await rescheduleAdminBooking('user-1', 'booking-1', {
|
||||
courtId: 'court-2',
|
||||
startTime: '14:00',
|
||||
});
|
||||
} catch (error) {
|
||||
caught = error;
|
||||
}
|
||||
|
||||
expect(caught).toBeInstanceOf(AdminBookingServiceError);
|
||||
expect((caught as InstanceType<typeof AdminBookingServiceError>).status).toBe(409);
|
||||
});
|
||||
|
||||
test('rejects when target court has a different sport', async () => {
|
||||
prismaMock.courtBooking.findFirst.mockResolvedValue(makeBooking() as never);
|
||||
prismaMock.court.findFirst.mockResolvedValue(
|
||||
makeCourt({
|
||||
sport: { id: 'sport-2', name: 'Fútbol', slug: 'futbol' },
|
||||
}) as never
|
||||
);
|
||||
|
||||
let caught: unknown;
|
||||
try {
|
||||
await rescheduleAdminBooking('user-1', 'booking-1', {
|
||||
courtId: 'court-2',
|
||||
startTime: '14:00',
|
||||
});
|
||||
} catch (error) {
|
||||
caught = error;
|
||||
}
|
||||
|
||||
expect(caught).toBeInstanceOf(AdminBookingServiceError);
|
||||
expect((caught as InstanceType<typeof AdminBookingServiceError>).status).toBe(409);
|
||||
expect(transactionMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('rejects when target court is under maintenance', async () => {
|
||||
prismaMock.courtBooking.findFirst.mockResolvedValue(makeBooking() as never);
|
||||
prismaMock.court.findFirst.mockResolvedValue(
|
||||
makeCourt({
|
||||
isUnderMaintenance: true,
|
||||
}) as never
|
||||
);
|
||||
|
||||
let caught: unknown;
|
||||
try {
|
||||
await rescheduleAdminBooking('user-1', 'booking-1', {
|
||||
courtId: 'court-2',
|
||||
startTime: '14:00',
|
||||
});
|
||||
} catch (error) {
|
||||
caught = error;
|
||||
}
|
||||
|
||||
expect(caught).toBeInstanceOf(AdminBookingServiceError);
|
||||
expect((caught as InstanceType<typeof AdminBookingServiceError>).status).toBe(409);
|
||||
});
|
||||
|
||||
test('rejects when there is an overlapping booking', async () => {
|
||||
prismaMock.courtBooking.findFirst
|
||||
.mockResolvedValueOnce(makeBooking() as never) // initial fetch
|
||||
.mockResolvedValueOnce({ id: 'overlap-1' } as never); // overlap found
|
||||
|
||||
prismaMock.court.findFirst.mockResolvedValue(
|
||||
makeCourt({
|
||||
availabilities: [
|
||||
{ id: 'avail-1', dayOfWeek: WEDNESDAY, startTime: '08:00', endTime: '18:00' },
|
||||
],
|
||||
}) as never
|
||||
);
|
||||
|
||||
let caught: unknown;
|
||||
try {
|
||||
await rescheduleAdminBooking('user-1', 'booking-1', {
|
||||
courtId: 'court-2',
|
||||
startTime: '14:00',
|
||||
});
|
||||
} catch (error) {
|
||||
caught = error;
|
||||
}
|
||||
|
||||
expect(caught).toBeInstanceOf(AdminBookingServiceError);
|
||||
expect((caught as InstanceType<typeof AdminBookingServiceError>).status).toBe(409);
|
||||
expect(transactionMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('rejects when neither courtId nor startTime changes', async () => {
|
||||
prismaMock.courtBooking.findFirst.mockResolvedValue(makeBooking() as never);
|
||||
|
||||
let caught: unknown;
|
||||
try {
|
||||
await rescheduleAdminBooking('user-1', 'booking-1', {});
|
||||
} catch (error) {
|
||||
caught = error;
|
||||
}
|
||||
|
||||
expect(caught).toBeInstanceOf(AdminBookingServiceError);
|
||||
expect((caught as InstanceType<typeof AdminBookingServiceError>).status).toBe(400);
|
||||
expect(transactionMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('rejects when target court does not belong to the same complex', async () => {
|
||||
prismaMock.courtBooking.findFirst.mockResolvedValue(makeBooking() as never);
|
||||
// Court belongs to a different complex
|
||||
prismaMock.court.findFirst.mockResolvedValue(null as never);
|
||||
|
||||
let caught: unknown;
|
||||
try {
|
||||
await rescheduleAdminBooking('user-1', 'booking-1', {
|
||||
courtId: 'other-complex-court',
|
||||
startTime: '14:00',
|
||||
});
|
||||
} catch (error) {
|
||||
caught = error;
|
||||
}
|
||||
|
||||
expect(caught).toBeInstanceOf(AdminBookingServiceError);
|
||||
expect((caught as InstanceType<typeof AdminBookingServiceError>).status).toBe(404);
|
||||
expect(transactionMock).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -14,6 +14,7 @@ function makeV1Rules(): PlanRules {
|
||||
publicBookingPage: false,
|
||||
advancedReports: false,
|
||||
whatsappReminders: false,
|
||||
fixedSlots: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -28,6 +29,7 @@ function makeV2Rules(overrides?: Record<string, { amount: number; currency: stri
|
||||
publicBookingPage: true,
|
||||
advancedReports: true,
|
||||
whatsappReminders: true,
|
||||
fixedSlots: true,
|
||||
},
|
||||
...(overrides ? { pricing: { overrides } } : {}),
|
||||
};
|
||||
|
||||
@@ -22,6 +22,11 @@ export const dbMock = {
|
||||
user: prismaMock.user,
|
||||
complexInvitation: prismaMock.complexInvitation,
|
||||
sport: prismaMock.sport,
|
||||
court: prismaMock.court,
|
||||
courtBooking: prismaMock.courtBooking,
|
||||
courtBookingLog: prismaMock.courtBookingLog,
|
||||
courtAvailability: prismaMock.courtAvailability,
|
||||
courtPriceRule: prismaMock.courtPriceRule,
|
||||
$transaction: transactionMock,
|
||||
};
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@ type PlanFormRules = {
|
||||
publicBookingPage: boolean;
|
||||
advancedReports: boolean;
|
||||
whatsappReminders: boolean;
|
||||
fixedSlots: boolean;
|
||||
};
|
||||
policies: {
|
||||
maxAdvanceBookingDays: number | '';
|
||||
@@ -85,6 +86,7 @@ function getDefaultRules(): PlanFormRules {
|
||||
publicBookingPage: false,
|
||||
advancedReports: false,
|
||||
whatsappReminders: false,
|
||||
fixedSlots: false,
|
||||
},
|
||||
policies: {
|
||||
maxAdvanceBookingDays: '',
|
||||
@@ -113,6 +115,7 @@ function initRules(plan?: Plan): PlanFormRules {
|
||||
publicBookingPage: (features?.publicBookingPage as boolean) ?? false,
|
||||
advancedReports: (features?.advancedReports as boolean) ?? false,
|
||||
whatsappReminders: (features?.whatsappReminders as boolean) ?? false,
|
||||
fixedSlots: (features?.fixedSlots as boolean) ?? false,
|
||||
},
|
||||
policies: {
|
||||
maxAdvanceBookingDays: (policies?.maxAdvanceBookingDays as number | undefined) ?? '',
|
||||
@@ -437,6 +440,7 @@ function PlanFormDialog({
|
||||
['publicBookingPage', 'Página de reservas pública'],
|
||||
['advancedReports', 'Reportes avanzados'],
|
||||
['whatsappReminders', 'Recordatorios por WhatsApp'],
|
||||
['fixedSlots', 'Turnos fijos'],
|
||||
] as const
|
||||
).map(([key, label]) => (
|
||||
<div key={key} className="flex items-center justify-between">
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import { ApiClientError, apiClient } from '@/lib/api-client';
|
||||
import type { AdminBooking, ComplexWithRole, Court } from '@repo/api-contract';
|
||||
import type {
|
||||
AdminBooking,
|
||||
ComplexWithRole,
|
||||
Court,
|
||||
PlanFeatureFlags,
|
||||
RecurringBookingGroup,
|
||||
RescheduleAdminBookingInput,
|
||||
} from '@repo/api-contract';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
type ReactNode,
|
||||
@@ -44,6 +51,16 @@ interface CreateManualBookingPayload {
|
||||
customerEmail: string;
|
||||
}
|
||||
|
||||
interface CreateRecurringBookingPayload {
|
||||
courtId: string;
|
||||
date: string;
|
||||
startTime: string;
|
||||
customerName: string;
|
||||
customerPhone: string;
|
||||
customerEmail: string;
|
||||
recurringEndDate?: string;
|
||||
}
|
||||
|
||||
interface BookingContextValue {
|
||||
complex: ComplexWithRole;
|
||||
courts: Court[];
|
||||
@@ -66,6 +83,8 @@ interface BookingContextValue {
|
||||
createBookingError: string | null;
|
||||
isCreatingBooking: boolean;
|
||||
bookingToolsOpen: boolean;
|
||||
planFeatures: PlanFeatureFlags | null;
|
||||
isRecurringEnabled: boolean;
|
||||
setSelectedDate: (date: string) => void;
|
||||
moveSelectedDate: (amount: number) => void;
|
||||
setSelectedSportId: (sportId: string) => void;
|
||||
@@ -73,9 +92,16 @@ interface BookingContextValue {
|
||||
setViewMode: (mode: BookingViewMode) => void;
|
||||
setVisibleTimeRange: (range: BookingTimeRange) => void;
|
||||
openCreateBooking: (draft?: Partial<BookingSlotDraft>) => void;
|
||||
recurringGroups: RecurringBookingGroup[];
|
||||
isRecurringGroupsLoading: boolean;
|
||||
cancelRecurringGroup: (groupId: string) => void;
|
||||
isCancellingRecurringGroup: boolean;
|
||||
refreshRecurringGroups: () => void;
|
||||
closeCreateBooking: () => void;
|
||||
createBooking: (payload: CreateManualBookingPayload) => Promise<void>;
|
||||
createRecurringBooking: (payload: CreateRecurringBookingPayload) => Promise<void>;
|
||||
updateBookingStatus: (bookingId: string, status: 'CANCELLED' | 'COMPLETED' | 'NOSHOW') => void;
|
||||
rescheduleBooking: (bookingId: string, input: RescheduleAdminBookingInput) => void;
|
||||
exportDayReport: () => void;
|
||||
openBookingTools: (segment?: BookingTimelineSegment) => void;
|
||||
closeBookingTools: () => void;
|
||||
@@ -245,6 +271,20 @@ export function BookingProvider({ children, complex }: BookingProviderProps) {
|
||||
queryFn: () => apiClient.courts.listByComplex(complex.id),
|
||||
});
|
||||
|
||||
const recurringGroupsQuery = useQuery({
|
||||
queryKey: ['recurring-groups', complex.id],
|
||||
queryFn: () => apiClient.adminBookings.listRecurringGroups(complex.id),
|
||||
enabled: viewMode === 'recurring',
|
||||
});
|
||||
|
||||
const cancelRecurringGroupMutation = useMutation({
|
||||
mutationFn: (groupId: string) => apiClient.adminBookings.cancelRecurringGroup(groupId),
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: ['recurring-groups', complex.id] });
|
||||
await queryClient.invalidateQueries({ queryKey: ['admin-bookings', complex.id] });
|
||||
},
|
||||
});
|
||||
|
||||
const bookingsQuery = useQuery({
|
||||
queryKey: ['admin-bookings', complex.id, selectedDate],
|
||||
queryFn: () =>
|
||||
@@ -325,6 +365,26 @@ export function BookingProvider({ children, complex }: BookingProviderProps) {
|
||||
},
|
||||
});
|
||||
|
||||
const createRecurringBookingMutation = useMutation({
|
||||
mutationFn: (payload: CreateRecurringBookingPayload) =>
|
||||
apiClient.adminBookings.createRecurring(complex.id, {
|
||||
date: payload.date,
|
||||
courtId: payload.courtId,
|
||||
startTime: payload.startTime,
|
||||
customerName: payload.customerName,
|
||||
customerPhone: payload.customerPhone,
|
||||
customerEmail: payload.customerEmail,
|
||||
isRecurring: true,
|
||||
recurringEndDate: payload.recurringEndDate,
|
||||
}),
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: ['admin-bookings', complex.id] });
|
||||
await queryClient.invalidateQueries({ queryKey: ['manual-booking-availability'] });
|
||||
setIsCreateBookingOpen(false);
|
||||
setSelectedSlot(null);
|
||||
},
|
||||
});
|
||||
|
||||
const updateStatusMutation = useMutation({
|
||||
mutationFn: (payload: { bookingId: string; status: 'CANCELLED' | 'COMPLETED' | 'NOSHOW' }) =>
|
||||
apiClient.adminBookings.updateStatus(payload.bookingId, { status: payload.status }),
|
||||
@@ -355,7 +415,26 @@ export function BookingProvider({ children, complex }: BookingProviderProps) {
|
||||
[selectedDate]
|
||||
);
|
||||
|
||||
const closeBookingTools = () => setBookingToolsOpen(false);
|
||||
const closeBookingTools = () => {
|
||||
setBookingToolsOpen(false);
|
||||
setSelectedSegment(null);
|
||||
};
|
||||
|
||||
const rescheduleMutation = useMutation({
|
||||
mutationFn: (payload: { bookingId: string; input: RescheduleAdminBookingInput }) =>
|
||||
apiClient.adminBookings.reschedule(payload.bookingId, payload.input),
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: ['admin-bookings', complex.id] });
|
||||
closeBookingTools();
|
||||
},
|
||||
});
|
||||
|
||||
const rescheduleBooking = useCallback(
|
||||
(bookingId: string, input: RescheduleAdminBookingInput) => {
|
||||
rescheduleMutation.mutate({ bookingId, input });
|
||||
},
|
||||
[rescheduleMutation]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const handleOpenCreateBooking = () => {
|
||||
@@ -413,6 +492,16 @@ export function BookingProvider({ children, complex }: BookingProviderProps) {
|
||||
URL.revokeObjectURL(url);
|
||||
}, [bookings, complex.complexSlug, selectedDate]);
|
||||
|
||||
const planFeatures = complex.planFeatures ?? null;
|
||||
const isRecurringEnabled = planFeatures?.fixedSlots ?? false;
|
||||
|
||||
const createRecurringBooking = useCallback(
|
||||
async (payload: CreateRecurringBookingPayload) => {
|
||||
await createRecurringBookingMutation.mutateAsync(payload);
|
||||
},
|
||||
[createRecurringBookingMutation]
|
||||
);
|
||||
|
||||
const value = useMemo<BookingContextValue>(
|
||||
() => ({
|
||||
complex,
|
||||
@@ -437,11 +526,24 @@ export function BookingProvider({ children, complex }: BookingProviderProps) {
|
||||
'No pudimos cargar el panel de reservas.'
|
||||
)
|
||||
: null,
|
||||
recurringGroups: recurringGroupsQuery.data?.groups ?? [],
|
||||
isRecurringGroupsLoading: recurringGroupsQuery.isLoading,
|
||||
cancelRecurringGroup: (groupId: string) => cancelRecurringGroupMutation.mutate(groupId),
|
||||
isCancellingRecurringGroup: cancelRecurringGroupMutation.isPending,
|
||||
refreshRecurringGroups: () =>
|
||||
queryClient.invalidateQueries({ queryKey: ['recurring-groups', complex.id] }),
|
||||
isCreateBookingOpen,
|
||||
createBookingError: createBookingMutation.isError
|
||||
? extractMessage(createBookingMutation.error, 'No pudimos crear la reserva.')
|
||||
: null,
|
||||
isCreatingBooking: createBookingMutation.isPending,
|
||||
createBookingError:
|
||||
createBookingMutation.isError || createRecurringBookingMutation.isError
|
||||
? extractMessage(
|
||||
createBookingMutation.error ?? createRecurringBookingMutation.error,
|
||||
'No pudimos crear la reserva.'
|
||||
)
|
||||
: null,
|
||||
isCreatingBooking:
|
||||
createBookingMutation.isPending || createRecurringBookingMutation.isPending,
|
||||
planFeatures,
|
||||
isRecurringEnabled,
|
||||
setSelectedDate,
|
||||
moveSelectedDate: (amount) => setSelectedDate((date) => addDaysIso(date, amount)),
|
||||
setSelectedSportId,
|
||||
@@ -451,7 +553,9 @@ export function BookingProvider({ children, complex }: BookingProviderProps) {
|
||||
openCreateBooking,
|
||||
closeCreateBooking,
|
||||
createBooking,
|
||||
createRecurringBooking,
|
||||
updateBookingStatus,
|
||||
rescheduleBooking,
|
||||
exportDayReport,
|
||||
bookingToolsOpen,
|
||||
openBookingTools,
|
||||
@@ -473,10 +577,16 @@ export function BookingProvider({ children, complex }: BookingProviderProps) {
|
||||
createBookingMutation.error,
|
||||
createBookingMutation.isError,
|
||||
createBookingMutation.isPending,
|
||||
createRecurringBooking,
|
||||
createRecurringBookingMutation.error,
|
||||
createRecurringBookingMutation.isError,
|
||||
createRecurringBookingMutation.isPending,
|
||||
currentTime,
|
||||
exportDayReport,
|
||||
isCreateBookingOpen,
|
||||
openCreateBooking,
|
||||
planFeatures,
|
||||
isRecurringEnabled,
|
||||
schedules,
|
||||
selectedDate,
|
||||
selectedSlot,
|
||||
@@ -485,9 +595,13 @@ export function BookingProvider({ children, complex }: BookingProviderProps) {
|
||||
summary,
|
||||
timelineHours,
|
||||
updateBookingStatus,
|
||||
rescheduleBooking,
|
||||
viewMode,
|
||||
visibleTimeRange,
|
||||
bookingToolsOpen,
|
||||
recurringGroupsQuery.data?.groups,
|
||||
recurringGroupsQuery.isLoading,
|
||||
cancelRecurringGroupMutation.isPending,
|
||||
]
|
||||
);
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import { BookingMobile } from './components/booking-mobile';
|
||||
import { BookingTimeline } from './components/booking-timeline';
|
||||
import { BookingToolbar } from './components/booking-toolbar';
|
||||
import { BookingToolsDialog } from './components/booking-tools-dialog';
|
||||
import { RecurringBookingListView } from './components/recurring-booking-list';
|
||||
|
||||
interface BookingProps {
|
||||
complex: ComplexWithRole;
|
||||
@@ -71,6 +72,10 @@ function BookingInner() {
|
||||
return <BookingListView />;
|
||||
}
|
||||
|
||||
if (viewMode === 'recurring') {
|
||||
return <RecurringBookingListView />;
|
||||
}
|
||||
|
||||
if (isMobile) {
|
||||
return <BookingMobile />;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { AdminBooking, Court } from '@repo/api-contract';
|
||||
|
||||
export type BookingViewMode = 'panel' | 'status' | 'list';
|
||||
export type BookingViewMode = 'panel' | 'status' | 'list' | 'recurring';
|
||||
export type BookingStatusFilter = 'all' | 'free' | 'reserved';
|
||||
export type BookingSegmentStatus = 'free' | 'reserved';
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
@@ -60,12 +61,16 @@ export function BookingCreateDialog() {
|
||||
isCreatingBooking,
|
||||
closeCreateBooking,
|
||||
createBooking,
|
||||
createRecurringBooking,
|
||||
isRecurringEnabled,
|
||||
} = useBooking();
|
||||
|
||||
const [date, setDate] = useState(selectedSlot?.date ?? selectedDate);
|
||||
const [sportId, setSportId] = useState(selectedSlot?.sportId ?? 'all');
|
||||
const [courtId, setCourtId] = useState(selectedSlot?.courtId ?? '');
|
||||
const [startTime, setStartTime] = useState(selectedSlot?.startTime ?? '');
|
||||
const [isRecurring, setIsRecurring] = useState(false);
|
||||
const [recurringEndDate, setRecurringEndDate] = useState<string | undefined>(undefined);
|
||||
|
||||
const {
|
||||
register,
|
||||
@@ -90,6 +95,8 @@ export function BookingCreateDialog() {
|
||||
setSportId(selectedSlot?.sportId ?? 'all');
|
||||
setCourtId(selectedSlot?.courtId ?? '');
|
||||
setStartTime(selectedSlot?.startTime ?? '');
|
||||
setIsRecurring(false);
|
||||
setRecurringEndDate(undefined);
|
||||
reset();
|
||||
|
||||
if (selectedSlot?.courtId && selectedSlot?.startTime) {
|
||||
@@ -155,14 +162,26 @@ export function BookingCreateDialog() {
|
||||
}, [availableStartTimes, startTime]);
|
||||
|
||||
const onSubmit = async (values: BookingForm) => {
|
||||
await createBooking({
|
||||
courtId,
|
||||
date,
|
||||
startTime,
|
||||
customerName: values.customerName,
|
||||
customerPhone: values.customerPhone,
|
||||
customerEmail: values.customerEmail,
|
||||
});
|
||||
if (isRecurring) {
|
||||
await createRecurringBooking({
|
||||
courtId,
|
||||
date,
|
||||
startTime,
|
||||
customerName: values.customerName,
|
||||
customerPhone: values.customerPhone,
|
||||
customerEmail: values.customerEmail,
|
||||
recurringEndDate,
|
||||
});
|
||||
} else {
|
||||
await createBooking({
|
||||
courtId,
|
||||
date,
|
||||
startTime,
|
||||
customerName: values.customerName,
|
||||
customerPhone: values.customerPhone,
|
||||
customerEmail: values.customerEmail,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -260,6 +279,52 @@ export function BookingCreateDialog() {
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
{isRecurringEnabled && (
|
||||
<>
|
||||
<Separator />
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<p className="text-sm font-medium">Repetir todas las semanas</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{isRecurring
|
||||
? `Se creará una reserva todos los ${getDayOfWeekLabel(date)} a las ${startTime || '...'}`
|
||||
: 'Crea turnos fijos en el mismo horario'}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant={isRecurring ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => setIsRecurring(!isRecurring)}
|
||||
className="shrink-0"
|
||||
>
|
||||
{isRecurring ? 'Activado' : 'Desactivado'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{isRecurring && (
|
||||
<Field>
|
||||
<FieldLabel>
|
||||
Repetir hasta{' '}
|
||||
<span className="text-muted-foreground font-normal">(opcional)</span>
|
||||
</FieldLabel>
|
||||
<DatePicker
|
||||
value={fromIsoDateLocal(recurringEndDate ?? '')}
|
||||
onChange={(nextDate) => {
|
||||
setRecurringEndDate(nextDate ? toIsoDateLocal(nextDate) : undefined);
|
||||
}}
|
||||
/>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
Si no se selecciona una fecha, se repetirá por tiempo indefinido.
|
||||
</p>
|
||||
</Field>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<Separator />
|
||||
|
||||
<Field data-invalid={Boolean(errors.customerName)}>
|
||||
<FieldLabel htmlFor="customerName">Nombre</FieldLabel>
|
||||
<Input
|
||||
@@ -312,10 +377,29 @@ export function BookingCreateDialog() {
|
||||
form="booking-create-form"
|
||||
disabled={!isValid || isCreatingBooking || !courtId || !startTime}
|
||||
>
|
||||
{isCreatingBooking ? 'Guardando...' : 'Crear reserva'}
|
||||
{isCreatingBooking
|
||||
? isRecurring
|
||||
? 'Creando turnos...'
|
||||
: 'Guardando...'
|
||||
: isRecurring
|
||||
? 'Crear turno fijo'
|
||||
: 'Crear reserva'}
|
||||
</Button>
|
||||
</ResponsiveDialogFooter>
|
||||
</ResponsiveDialogContent>
|
||||
</ResponsiveDialog>
|
||||
);
|
||||
}
|
||||
|
||||
function getDayOfWeekLabel(dateIso: string): string {
|
||||
const labels: Record<string, string> = {
|
||||
MONDAY: 'lunes',
|
||||
TUESDAY: 'martes',
|
||||
WEDNESDAY: 'miércoles',
|
||||
THURSDAY: 'jueves',
|
||||
FRIDAY: 'viernes',
|
||||
SATURDAY: 'sábado',
|
||||
SUNDAY: 'domingo',
|
||||
};
|
||||
return labels[getDayOfWeek(dateIso)] ?? '';
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CalendarDays, Download, ShieldCheck, UsersRound } from 'lucide-react';
|
||||
import { CalendarDays, Download, Repeat, ShieldCheck, UsersRound } from 'lucide-react';
|
||||
import { useBooking } from '../booking-provider';
|
||||
import { formatBookingDate, toIsoDateLocal } from '../lib/booking-time';
|
||||
|
||||
@@ -66,7 +66,7 @@ function SummaryCard({ label, value, detail, icon: Icon, className }: SummaryCar
|
||||
}
|
||||
|
||||
export function BookingQuickActions() {
|
||||
const { setSelectedDate, setViewMode, exportDayReport } = useBooking();
|
||||
const { setSelectedDate, setViewMode, exportDayReport, isRecurringEnabled } = useBooking();
|
||||
const todayIso = toIsoDateLocal(new Date());
|
||||
|
||||
return (
|
||||
@@ -92,6 +92,16 @@ export function BookingQuickActions() {
|
||||
<Download className="size-4 text-primary" />
|
||||
Exportar reporte
|
||||
</button>
|
||||
{isRecurringEnabled && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setViewMode('recurring')}
|
||||
className="flex h-10 items-center gap-3 rounded-md border bg-background/45 px-3 text-left text-sm transition-colors hover:bg-muted"
|
||||
>
|
||||
<Repeat className="size-4 text-muted-foreground" />
|
||||
Ver turnos fijos
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
ResponsiveDialog,
|
||||
ResponsiveDialogClose,
|
||||
ResponsiveDialogContent,
|
||||
ResponsiveDialogDescription,
|
||||
ResponsiveDialogFooter,
|
||||
ResponsiveDialogHeader,
|
||||
ResponsiveDialogTitle,
|
||||
} from '@/components/ui/responsive-dialog';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import type { AdminBooking } from '@repo/api-contract';
|
||||
import { CalendarSync } from 'lucide-react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useBooking } from '../booking-provider';
|
||||
import {
|
||||
getDayOfWeek,
|
||||
getNowTime,
|
||||
isTodayIso,
|
||||
minutesToTime,
|
||||
timeToMinutes,
|
||||
} from '../lib/booking-time';
|
||||
|
||||
interface BookingRescheduleDialogProps {
|
||||
booking: AdminBooking;
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export function BookingRescheduleDialog({
|
||||
booking,
|
||||
open,
|
||||
onOpenChange,
|
||||
}: BookingRescheduleDialogProps) {
|
||||
const { courts, bookings, rescheduleBooking } = useBooking();
|
||||
|
||||
const [courtId, setCourtId] = useState(booking.courtId);
|
||||
const [startTime, setStartTime] = useState(booking.startTime);
|
||||
|
||||
const sportId = booking.sport.id;
|
||||
const filteredCourts = useMemo(
|
||||
() => courts.filter((c) => c.sportId === sportId),
|
||||
[courts, sportId]
|
||||
);
|
||||
|
||||
const selectedCourt = filteredCourts.find((c) => c.id === courtId);
|
||||
const dayOfWeek = getDayOfWeek(booking.date);
|
||||
|
||||
const availableStartTimes = useMemo(() => {
|
||||
if (!selectedCourt) return [];
|
||||
|
||||
const times: string[] = [];
|
||||
const dayAvailability = selectedCourt.availability
|
||||
.filter((range) => range.dayOfWeek === dayOfWeek)
|
||||
.sort((a, b) => timeToMinutes(a.startTime) - timeToMinutes(b.startTime));
|
||||
|
||||
const courtBookings = bookings.filter(
|
||||
(b) =>
|
||||
b.date === booking.date &&
|
||||
b.courtId === selectedCourt.id &&
|
||||
b.id !== booking.id &&
|
||||
b.status !== 'CANCELLED'
|
||||
);
|
||||
|
||||
for (const range of dayAvailability) {
|
||||
const start = timeToMinutes(range.startTime);
|
||||
const end = timeToMinutes(range.endTime);
|
||||
|
||||
for (
|
||||
let minute = start;
|
||||
minute + selectedCourt.slotDurationMinutes <= end;
|
||||
minute += selectedCourt.slotDurationMinutes
|
||||
) {
|
||||
const slotEnd = minute + selectedCourt.slotDurationMinutes;
|
||||
|
||||
if (isTodayIso(booking.date) && minute <= timeToMinutes(getNowTime())) continue;
|
||||
|
||||
const isBooked = courtBookings.some((b) => {
|
||||
const bookingStart = timeToMinutes(b.startTime);
|
||||
const bookingEnd = timeToMinutes(b.endTime);
|
||||
return minute < bookingEnd && slotEnd > bookingStart;
|
||||
});
|
||||
|
||||
if (!isBooked) {
|
||||
times.push(minutesToTime(minute));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return times;
|
||||
}, [bookings, booking.date, booking.id, dayOfWeek, selectedCourt]);
|
||||
|
||||
const hasChanges = courtId !== booking.courtId || startTime !== booking.startTime;
|
||||
|
||||
const handleConfirm = () => {
|
||||
if (!hasChanges) return;
|
||||
|
||||
const payload: Record<string, string> = {};
|
||||
if (courtId !== booking.courtId) payload.courtId = courtId;
|
||||
if (startTime !== booking.startTime) payload.startTime = startTime;
|
||||
|
||||
rescheduleBooking(booking.id, payload);
|
||||
onOpenChange(false);
|
||||
};
|
||||
|
||||
const handleOpenChange = (newOpen: boolean) => {
|
||||
if (newOpen) {
|
||||
setCourtId(booking.courtId);
|
||||
setStartTime(booking.startTime);
|
||||
}
|
||||
onOpenChange(newOpen);
|
||||
};
|
||||
|
||||
return (
|
||||
<ResponsiveDialog open={open} onOpenChange={handleOpenChange}>
|
||||
<ResponsiveDialogContent>
|
||||
<ResponsiveDialogHeader>
|
||||
<ResponsiveDialogTitle>Reprogramar reserva</ResponsiveDialogTitle>
|
||||
<ResponsiveDialogDescription>
|
||||
Cambiá la cancha y/o el horario de la reserva para el mismo día.
|
||||
</ResponsiveDialogDescription>
|
||||
</ResponsiveDialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="rounded-lg border bg-card p-3 text-sm text-muted-foreground">
|
||||
Reserva <strong>{booking.bookingCode}</strong> — {booking.customerName}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-medium">Cancha</p>
|
||||
<Select value={courtId} onValueChange={setCourtId}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Seleccionar cancha" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{filteredCourts.map((court) => (
|
||||
<SelectItem key={court.id} value={court.id} disabled={court.isUnderMaintenance}>
|
||||
<span className="flex items-center gap-2">
|
||||
{court.name} — {court.sport.name}
|
||||
{court.isUnderMaintenance && (
|
||||
<span className="ml-auto text-xs text-muted-foreground">
|
||||
(En mantenimiento)
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-medium">Horario</p>
|
||||
<Select
|
||||
value={startTime}
|
||||
onValueChange={setStartTime}
|
||||
disabled={!selectedCourt || availableStartTimes.length === 0}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue
|
||||
placeholder={
|
||||
selectedCourt
|
||||
? availableStartTimes.length === 0
|
||||
? 'Sin horarios disponibles'
|
||||
: 'Seleccionar horario'
|
||||
: 'Seleccioná una cancha primero'
|
||||
}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{availableStartTimes.map((time) => {
|
||||
const slotMinutes = timeToMinutes(time);
|
||||
const endMinutes = slotMinutes + (selectedCourt?.slotDurationMinutes ?? 0);
|
||||
return (
|
||||
<SelectItem key={time} value={time}>
|
||||
{time} — {minutesToTime(endMinutes)}
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ResponsiveDialogFooter>
|
||||
<ResponsiveDialogClose asChild>
|
||||
<Button variant="outline">Cancelar</Button>
|
||||
</ResponsiveDialogClose>
|
||||
<Button onClick={handleConfirm} disabled={!hasChanges || !startTime || !courtId}>
|
||||
<CalendarSync className="mr-2 h-4 w-4" />
|
||||
Confirmar reprogramación
|
||||
</Button>
|
||||
</ResponsiveDialogFooter>
|
||||
</ResponsiveDialogContent>
|
||||
</ResponsiveDialog>
|
||||
);
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
AlertTriangle,
|
||||
BadgeCheck,
|
||||
CalendarDays,
|
||||
CalendarSync,
|
||||
CheckCircle2,
|
||||
Clock,
|
||||
MapPin,
|
||||
@@ -26,6 +27,7 @@ import {
|
||||
} from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { useBooking } from '../booking-provider';
|
||||
import { BookingRescheduleDialog } from './booking-reschedule-dialog';
|
||||
|
||||
function formatDate(date: string | undefined) {
|
||||
if (!date) return '';
|
||||
@@ -72,6 +74,7 @@ export function BookingToolsDialog() {
|
||||
const isMobile = useIsMobile();
|
||||
const booking = selectedSegment?.booking;
|
||||
const [confirmCancelOpen, setConfirmCancelOpen] = useState(false);
|
||||
const [rescheduleOpen, setRescheduleOpen] = useState(false);
|
||||
|
||||
const status = statusConfig[booking?.status ?? 'CONFIRMED'] ?? {
|
||||
label: booking?.status,
|
||||
@@ -165,10 +168,19 @@ export function BookingToolsDialog() {
|
||||
onCancel={() => setConfirmCancelOpen(true)}
|
||||
onReminder={sendWhatsappReminder}
|
||||
onNoShow={() => updateStatus('NOSHOW')}
|
||||
onReschedule={() => setRescheduleOpen(true)}
|
||||
/>
|
||||
</ResponsiveDialogContent>
|
||||
</ResponsiveDialog>
|
||||
{cancelConfirmDialog}
|
||||
|
||||
{booking && (
|
||||
<BookingRescheduleDialog
|
||||
booking={booking}
|
||||
open={rescheduleOpen}
|
||||
onOpenChange={setRescheduleOpen}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -268,6 +280,14 @@ export function BookingToolsDialog() {
|
||||
onClick={() => setConfirmCancelOpen(true)}
|
||||
/>
|
||||
|
||||
<ActionButton
|
||||
icon={<CalendarSync className="h-5 w-5" />}
|
||||
title="Reprogramar"
|
||||
description="Cambiar la cancha y/o el horario de la reserva."
|
||||
className="border-violet-500/40 bg-violet-500/10 text-lg text-violet-600 hover:bg-violet-500/15 hover:text-violet-800"
|
||||
onClick={() => setRescheduleOpen(true)}
|
||||
/>
|
||||
|
||||
<ActionButton
|
||||
icon={<MessageCircle className="h-5 w-5" />}
|
||||
title="Enviar recordatorio"
|
||||
@@ -299,6 +319,14 @@ export function BookingToolsDialog() {
|
||||
</ResponsiveDialogContent>
|
||||
</ResponsiveDialog>
|
||||
{cancelConfirmDialog}
|
||||
|
||||
{booking && (
|
||||
<BookingRescheduleDialog
|
||||
booking={booking}
|
||||
open={rescheduleOpen}
|
||||
onOpenChange={setRescheduleOpen}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -309,12 +337,14 @@ function MobileBookingToolsSheet({
|
||||
onCancel,
|
||||
onReminder,
|
||||
onNoShow,
|
||||
onReschedule,
|
||||
}: {
|
||||
status: { label: string | undefined; className: string };
|
||||
onComplete: () => void;
|
||||
onCancel: () => void;
|
||||
onReminder: () => void;
|
||||
onNoShow: () => void;
|
||||
onReschedule: () => void;
|
||||
}) {
|
||||
const { selectedSegment } = useBooking();
|
||||
const booking = selectedSegment?.booking;
|
||||
@@ -395,6 +425,12 @@ function MobileBookingToolsSheet({
|
||||
className="border-primary/45 bg-primary/12 text-primary"
|
||||
onClick={onComplete}
|
||||
/>
|
||||
<MobileActionButton
|
||||
icon={<CalendarSync className="size-5" />}
|
||||
label="Reprogramar"
|
||||
className="border-violet-500/45 bg-violet-500/12 text-violet-500"
|
||||
onClick={onReschedule}
|
||||
/>
|
||||
<MobileActionButton
|
||||
icon={<MessageCircle className="size-5" />}
|
||||
label="Recordar"
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { apiClient } from '@/lib/api-client';
|
||||
import type { RecurringBookingGroup } from '@repo/api-contract';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useBooking } from '../booking-provider';
|
||||
import { minutesToTime, timeToMinutes } from '../lib/booking-time';
|
||||
|
||||
const DAY_OPTIONS = [
|
||||
{ value: 'MONDAY', label: 'Lunes' },
|
||||
{ value: 'TUESDAY', label: 'Martes' },
|
||||
{ value: 'WEDNESDAY', label: 'Miércoles' },
|
||||
{ value: 'THURSDAY', label: 'Jueves' },
|
||||
{ value: 'FRIDAY', label: 'Viernes' },
|
||||
{ value: 'SATURDAY', label: 'Sábado' },
|
||||
{ value: 'SUNDAY', label: 'Domingo' },
|
||||
];
|
||||
|
||||
interface RecurringBookingEditDialogProps {
|
||||
group: RecurringBookingGroup;
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function RecurringBookingEditDialog({
|
||||
group,
|
||||
open,
|
||||
onClose,
|
||||
}: RecurringBookingEditDialogProps) {
|
||||
const { courts, refreshRecurringGroups } = useBooking();
|
||||
|
||||
const [courtId, setCourtId] = useState<string>(group.courtId);
|
||||
const [dayOfWeek, setDayOfWeek] = useState<string>(group.dayOfWeek);
|
||||
const [startTime, setStartTime] = useState<string>(group.startTime);
|
||||
const [customerName, setCustomerName] = useState(group.customerName);
|
||||
const [customerPhone, setCustomerPhone] = useState(group.customerPhone);
|
||||
const [customerEmail, setCustomerEmail] = useState(group.customerEmail);
|
||||
const [error, setError] = useState('');
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setCourtId(group.courtId);
|
||||
setDayOfWeek(group.dayOfWeek);
|
||||
setStartTime(group.startTime);
|
||||
setCustomerName(group.customerName);
|
||||
setCustomerPhone(group.customerPhone);
|
||||
setCustomerEmail(group.customerEmail);
|
||||
}, [group]);
|
||||
|
||||
const selectedCourt = courts.find((c) => c.id === courtId);
|
||||
const slotDuration = selectedCourt?.slotDurationMinutes ?? 60;
|
||||
|
||||
const availableSlots: string[] = [];
|
||||
if (selectedCourt) {
|
||||
const availabilities = selectedCourt.availability.filter((a) => a.dayOfWeek === dayOfWeek);
|
||||
for (const avail of availabilities) {
|
||||
const startMins = timeToMinutes(avail.startTime);
|
||||
const endMins = timeToMinutes(avail.endTime);
|
||||
for (let m = startMins; m + slotDuration <= endMins; m += slotDuration) {
|
||||
availableSlots.push(minutesToTime(m));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!customerName || customerName.trim().length < 2) {
|
||||
setError('El nombre debe tener al menos 2 caracteres.');
|
||||
return;
|
||||
}
|
||||
|
||||
setError('');
|
||||
setSaving(true);
|
||||
|
||||
try {
|
||||
await apiClient.adminBookings.updateRecurringGroup(group.id, {
|
||||
...(courtId !== group.courtId && { courtId }),
|
||||
...(dayOfWeek !== group.dayOfWeek && {
|
||||
dayOfWeek: dayOfWeek as
|
||||
| 'MONDAY'
|
||||
| 'TUESDAY'
|
||||
| 'WEDNESDAY'
|
||||
| 'THURSDAY'
|
||||
| 'FRIDAY'
|
||||
| 'SATURDAY'
|
||||
| 'SUNDAY',
|
||||
}),
|
||||
...(startTime !== group.startTime && { startTime }),
|
||||
...(customerName !== group.customerName && { customerName }),
|
||||
...(customerPhone !== group.customerPhone && { customerPhone }),
|
||||
...(customerEmail !== group.customerEmail && { customerEmail }),
|
||||
});
|
||||
|
||||
await refreshRecurringGroups();
|
||||
onClose();
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : 'Error al guardar los cambios.');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={(open) => !open && onClose()}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Editar turno fijo</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-1.5">
|
||||
<Label>Cliente</Label>
|
||||
<Input
|
||||
value={customerName}
|
||||
onChange={(e) => setCustomerName(e.target.value)}
|
||||
placeholder="Nombre del cliente"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label>Teléfono</Label>
|
||||
<Input
|
||||
value={customerPhone}
|
||||
onChange={(e) => setCustomerPhone(e.target.value)}
|
||||
placeholder="Teléfono"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label>Email</Label>
|
||||
<Input
|
||||
value={customerEmail}
|
||||
onChange={(e) => setCustomerEmail(e.target.value)}
|
||||
placeholder="Email"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="space-y-1.5">
|
||||
<Label>Cancha</Label>
|
||||
<Select value={courtId} onValueChange={setCourtId}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Cancha" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{courts.map((court) => (
|
||||
<SelectItem key={court.id} value={court.id}>
|
||||
{court.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label>Día</Label>
|
||||
<Select value={dayOfWeek} onValueChange={setDayOfWeek}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Día" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{DAY_OPTIONS.map((day) => (
|
||||
<SelectItem key={day.value} value={day.value}>
|
||||
{day.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label>Horario</Label>
|
||||
<Select value={startTime} onValueChange={setStartTime}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Horario" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{availableSlots.map((slot) => (
|
||||
<SelectItem key={slot} value={slot}>
|
||||
{slot} - {minutesToTime(timeToMinutes(slot) + slotDuration)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{error && <p className="text-sm text-destructive">{error}</p>}
|
||||
|
||||
<div className="flex justify-end gap-3 pt-2">
|
||||
<Button type="button" variant="outline" onClick={onClose}>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button type="button" onClick={handleSave} disabled={saving}>
|
||||
{saving ? 'Guardando...' : 'Guardar cambios'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import type { RecurringBookingGroup } from '@repo/api-contract';
|
||||
import { ArrowLeft, Clock, MapPin, Pencil, Repeat, Trash2, User } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { useBooking } from '../booking-provider';
|
||||
import { RecurringBookingEditDialog } from './recurring-booking-edit-dialog';
|
||||
|
||||
const DAY_LABELS: Record<string, string> = {
|
||||
MONDAY: 'Lunes',
|
||||
TUESDAY: 'Martes',
|
||||
WEDNESDAY: 'Miércoles',
|
||||
THURSDAY: 'Jueves',
|
||||
FRIDAY: 'Viernes',
|
||||
SATURDAY: 'Sábado',
|
||||
SUNDAY: 'Domingo',
|
||||
};
|
||||
|
||||
function getNextBookingDate(group: RecurringBookingGroup): string {
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
const future = group.bookings
|
||||
.filter((b) => b.date >= today && b.status === 'CONFIRMED')
|
||||
.sort((a, b) => a.date.localeCompare(b.date));
|
||||
return future[0]?.date ?? '-';
|
||||
}
|
||||
|
||||
function formatDate(dateStr: string): string {
|
||||
if (dateStr === '-') return '-';
|
||||
const [y, m, d] = dateStr.split('-');
|
||||
return `${d}/${m}/${y}`;
|
||||
}
|
||||
|
||||
export function RecurringBookingListView() {
|
||||
const {
|
||||
setViewMode,
|
||||
recurringGroups,
|
||||
isRecurringGroupsLoading,
|
||||
cancelRecurringGroup,
|
||||
isCancellingRecurringGroup,
|
||||
} = useBooking();
|
||||
|
||||
const [editingGroup, setEditingGroup] = useState<RecurringBookingGroup | null>(null);
|
||||
const [cancellingGroupId, setCancellingGroupId] = useState<string | null>(null);
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className="rounded-lg border bg-card/85 shadow-sm">
|
||||
<div className="flex flex-wrap items-center justify-between gap-4 border-b px-4 py-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="rounded-full"
|
||||
onClick={() => setViewMode('panel')}
|
||||
>
|
||||
<ArrowLeft className="size-5" />
|
||||
</Button>
|
||||
<div>
|
||||
<h2 className="text-base font-semibold">Turnos fijos</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{recurringGroups.length}{' '}
|
||||
{recurringGroups.length === 1 ? 'turno fijo' : 'turnos fijos'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isRecurringGroupsLoading && (
|
||||
<div className="px-4 py-10 text-sm text-muted-foreground">Cargando turnos fijos...</div>
|
||||
)}
|
||||
|
||||
{!isRecurringGroupsLoading && recurringGroups.length === 0 && (
|
||||
<div className="px-4 py-10 text-center text-sm text-muted-foreground">
|
||||
<Repeat className="mx-auto mb-3 size-10 opacity-40" />
|
||||
<p>No hay turnos fijos creados.</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isRecurringGroupsLoading && recurringGroups.length > 0 && (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="hidden w-full sm:table">
|
||||
<thead>
|
||||
<tr className="border-b text-left text-xs font-medium uppercase tracking-wider text-muted-foreground">
|
||||
<th className="px-4 py-3 font-medium">Cliente</th>
|
||||
<th className="px-4 py-3 font-medium">Cancha</th>
|
||||
<th className="px-4 py-3 font-medium">Día</th>
|
||||
<th className="px-4 py-3 font-medium">Horario</th>
|
||||
<th className="px-4 py-3 font-medium">Próxima fecha</th>
|
||||
<th className="px-4 py-3 font-medium">Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y">
|
||||
{recurringGroups.map((group) => {
|
||||
const nextDate = getNextBookingDate(group);
|
||||
return (
|
||||
<tr key={group.id} className="transition-colors hover:bg-muted/50">
|
||||
<td className="px-4 py-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<User className="size-4 shrink-0 text-muted-foreground" />
|
||||
<span className="truncate text-sm">{group.customerName || '-'}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<MapPin className="size-4 shrink-0 text-muted-foreground" />
|
||||
<span className="truncate text-sm">
|
||||
{group.bookings[0]?.courtName ?? '-'}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-4 text-sm">
|
||||
{DAY_LABELS[group.dayOfWeek] ?? group.dayOfWeek}
|
||||
</td>
|
||||
<td className="px-4 py-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Clock className="size-4 shrink-0 text-muted-foreground" />
|
||||
<span className="text-sm">
|
||||
{group.startTime} - {group.endTime}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-4 text-sm">{formatDate(nextDate)}</td>
|
||||
<td className="px-4 py-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setEditingGroup(group)}
|
||||
className="flex items-center gap-1.5 rounded-md border px-2.5 py-1.5 text-xs font-medium transition-colors hover:bg-muted"
|
||||
>
|
||||
<Pencil className="size-3.5" />
|
||||
Editar
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setCancellingGroupId(group.id)}
|
||||
disabled={isCancellingRecurringGroup}
|
||||
className="flex items-center gap-1.5 rounded-md border border-red-200 px-2.5 py-1.5 text-xs font-medium text-red-600 transition-colors hover:bg-red-50 disabled:opacity-50"
|
||||
>
|
||||
<Trash2 className="size-3.5" />
|
||||
Cancelar
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div className="divide-y sm:hidden">
|
||||
{recurringGroups.map((group) => {
|
||||
const nextDate = getNextBookingDate(group);
|
||||
return (
|
||||
<div key={group.id} className="px-4 py-4">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<User className="size-4 text-muted-foreground" />
|
||||
<span className="text-sm font-semibold">{group.customerName || '-'}</span>
|
||||
</div>
|
||||
<span className="rounded-full bg-primary/10 px-2 py-0.5 text-xs font-medium text-primary">
|
||||
{DAY_LABELS[group.dayOfWeek]}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{group.bookings[0]?.courtName} · {group.startTime} - {group.endTime}
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
Próxima: {formatDate(nextDate)}
|
||||
</p>
|
||||
<div className="mt-2 flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setEditingGroup(group)}
|
||||
className="flex items-center gap-1 rounded-md border px-2 py-1 text-xs transition-colors hover:bg-muted"
|
||||
>
|
||||
<Pencil className="size-3" />
|
||||
Editar
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setCancellingGroupId(group.id)}
|
||||
disabled={isCancellingRecurringGroup}
|
||||
className="flex items-center gap-1 rounded-md border border-red-200 px-2 py-1 text-xs text-red-600 transition-colors hover:bg-red-50 disabled:opacity-50"
|
||||
>
|
||||
<Trash2 className="size-3" />
|
||||
Cancelar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<Dialog
|
||||
open={!!cancellingGroupId}
|
||||
onOpenChange={(open) => !open && setCancellingGroupId(null)}
|
||||
>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Cancelar turno fijo</DialogTitle>
|
||||
<DialogDescription>
|
||||
¿Estás seguro de cancelar este turno fijo? Se eliminarán todas las reservas futuras.
|
||||
Las reservas pasadas no se verán afectadas.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setCancellingGroupId(null)}>
|
||||
Volver
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
disabled={isCancellingRecurringGroup}
|
||||
onClick={() => {
|
||||
if (cancellingGroupId) {
|
||||
cancelRecurringGroup(cancellingGroupId);
|
||||
setCancellingGroupId(null);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{isCancellingRecurringGroup ? 'Cancelando...' : 'Sí, cancelar turno fijo'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{editingGroup && (
|
||||
<RecurringBookingEditDialog
|
||||
group={editingGroup}
|
||||
open={!!editingGroup}
|
||||
onClose={() => setEditingGroup(null)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -13,6 +13,7 @@ const FEATURE_LABELS: Record<string, string> = {
|
||||
onlinePayments: 'Pagos online',
|
||||
advancedReports: 'Reportes avanzados',
|
||||
whatsappReminders: 'Recordatorios WhatsApp',
|
||||
fixedSlots: 'Turnos fijos',
|
||||
};
|
||||
|
||||
export function PlanSelectStep({ form, plans }: PlanSelectStepProps) {
|
||||
@@ -24,6 +25,7 @@ export function PlanSelectStep({ form, plans }: PlanSelectStepProps) {
|
||||
'onlinePayments',
|
||||
'advancedReports',
|
||||
'whatsappReminders',
|
||||
'fixedSlots',
|
||||
] as const;
|
||||
|
||||
return (
|
||||
|
||||
@@ -74,6 +74,11 @@ export const apiClient = {
|
||||
adminBookings: {
|
||||
listByComplex: api.listByComplex,
|
||||
create: api.createAdmin,
|
||||
createRecurring: api.createRecurring,
|
||||
cancelRecurringGroup: api.cancelRecurringGroup,
|
||||
listRecurringGroups: api.listRecurringGroups,
|
||||
reschedule: api.reschedule,
|
||||
updateRecurringGroup: api.updateRecurringGroup,
|
||||
updateStatus: api.updateStatus,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -15,5 +15,10 @@ export {
|
||||
getConfirmation,
|
||||
listByComplex,
|
||||
createAdmin,
|
||||
createRecurring,
|
||||
cancelRecurringGroup,
|
||||
listRecurringGroups,
|
||||
reschedule,
|
||||
updateRecurringGroup,
|
||||
updateStatus,
|
||||
} from './resources/bookings';
|
||||
|
||||
@@ -3,11 +3,16 @@ import type {
|
||||
CancelPublicBookingInput,
|
||||
CreateAdminBookingInput,
|
||||
CreatePublicBookingInput,
|
||||
ListRecurringGroupsResponse,
|
||||
PublicAvailabilityResponse,
|
||||
PublicBooking,
|
||||
PublicBookingConfirmation,
|
||||
RecurringBookingGroup,
|
||||
RescheduleAdminBookingInput,
|
||||
UpdateAdminBookingStatusInput,
|
||||
UpdateRecurringGroupInput,
|
||||
} from '@repo/api-contract';
|
||||
import type { CreateRecurringBookingInput } from '@repo/api-contract';
|
||||
import { http } from '../http';
|
||||
|
||||
export async function getAvailability(
|
||||
@@ -52,6 +57,36 @@ export async function createAdmin(complexId: string, payload: CreateAdminBooking
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function createRecurring(complexId: string, payload: CreateRecurringBookingInput) {
|
||||
const response = await http.post<RecurringBookingGroup>(
|
||||
`/api/admin-bookings/complex/${complexId}/recurring`,
|
||||
payload
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function cancelRecurringGroup(groupId: string) {
|
||||
const response = await http.post<{ ok: boolean }>(
|
||||
`/api/admin-bookings/recurring/${groupId}/cancel`
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function listRecurringGroups(complexId: string) {
|
||||
const response = await http.get<ListRecurringGroupsResponse>(
|
||||
`/api/admin-bookings/complex/${complexId}/recurring`
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function updateRecurringGroup(groupId: string, payload: UpdateRecurringGroupInput) {
|
||||
const response = await http.patch<RecurringBookingGroup>(
|
||||
`/api/admin-bookings/recurring/${groupId}`,
|
||||
payload
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function cancelPublic(complexSlug: string, payload: CancelPublicBookingInput) {
|
||||
const response = await http.post<PublicBooking>(
|
||||
`/api/public-bookings/complex/${complexSlug}/cancel`,
|
||||
@@ -67,3 +102,11 @@ export async function updateStatus(bookingId: string, payload: UpdateAdminBookin
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function reschedule(bookingId: string, payload: RescheduleAdminBookingInput) {
|
||||
const response = await http.patch<AdminBooking>(
|
||||
`/api/admin-bookings/${bookingId}/reschedule`,
|
||||
payload
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import { z } from 'zod'
|
||||
import { z } from 'zod';
|
||||
|
||||
const TIME_REGEX = /^([01]\d|2[0-3]):([0-5]\d)$/
|
||||
const ISO_DATE_REGEX = /^\d{4}-\d{2}-\d{2}$/
|
||||
const BOOKING_CODE_REGEX = /^[A-Z0-9]{6,8}$/
|
||||
const TIME_REGEX = /^([01]\d|2[0-3]):([0-5]\d)$/;
|
||||
const ISO_DATE_REGEX = /^\d{4}-\d{2}-\d{2}$/;
|
||||
const BOOKING_CODE_REGEX = /^[A-Z0-9]{6,8}$/;
|
||||
|
||||
export const bookingStatusSchema = z.enum(['CONFIRMED', 'CANCELLED', 'COMPLETED', 'NOSHOW'])
|
||||
export const bookingStatusSchema = z.enum(['CONFIRMED', 'CANCELLED', 'COMPLETED', 'NOSHOW']);
|
||||
|
||||
export const recurringGroupStatusSchema = z.enum(['ACTIVE', 'CANCELLED']);
|
||||
|
||||
export const adminBookingSportSchema = z.object({
|
||||
id: z.uuid(),
|
||||
name: z.string(),
|
||||
slug: z.string(),
|
||||
})
|
||||
});
|
||||
|
||||
export const adminBookingSchema = z.object({
|
||||
id: z.uuid(),
|
||||
@@ -28,17 +30,18 @@ export const adminBookingSchema = z.object({
|
||||
customerEmail: z.string(),
|
||||
price: z.number().nonnegative(),
|
||||
status: bookingStatusSchema,
|
||||
recurringGroupId: z.string().uuid().nullable().optional(),
|
||||
createdAt: z.string().datetime(),
|
||||
updatedAt: z.string().datetime(),
|
||||
})
|
||||
});
|
||||
|
||||
export const listAdminBookingsQuerySchema = z.object({
|
||||
fromDate: z.string().regex(ISO_DATE_REGEX, 'La fecha debe tener formato YYYY-MM-DD.'),
|
||||
})
|
||||
});
|
||||
|
||||
export const listAdminBookingsResponseSchema = z.object({
|
||||
bookings: z.array(adminBookingSchema),
|
||||
})
|
||||
});
|
||||
|
||||
export const createAdminBookingSchema = z.object({
|
||||
date: z.string().regex(ISO_DATE_REGEX, 'La fecha debe tener formato YYYY-MM-DD.'),
|
||||
@@ -54,20 +57,87 @@ export const createAdminBookingSchema = z.object({
|
||||
.trim()
|
||||
.min(6, 'El telefono debe tener al menos 6 caracteres.')
|
||||
.max(30, 'El telefono no puede superar los 30 caracteres.'),
|
||||
customerEmail: z
|
||||
.string()
|
||||
.email('El email ingresado no es valido.')
|
||||
.or(z.literal('')),
|
||||
})
|
||||
customerEmail: z.string().email('El email ingresado no es valido.').or(z.literal('')),
|
||||
});
|
||||
|
||||
export const updateAdminBookingStatusSchema = z.object({
|
||||
status: z.enum(['CANCELLED', 'COMPLETED', 'NOSHOW']),
|
||||
})
|
||||
});
|
||||
|
||||
export type BookingStatus = z.infer<typeof bookingStatusSchema>
|
||||
export type AdminBookingSport = z.infer<typeof adminBookingSportSchema>
|
||||
export type AdminBooking = z.infer<typeof adminBookingSchema>
|
||||
export type ListAdminBookingsQuery = z.infer<typeof listAdminBookingsQuerySchema>
|
||||
export type ListAdminBookingsResponse = z.infer<typeof listAdminBookingsResponseSchema>
|
||||
export type CreateAdminBookingInput = z.infer<typeof createAdminBookingSchema>
|
||||
export type UpdateAdminBookingStatusInput = z.infer<typeof updateAdminBookingStatusSchema>
|
||||
export const createRecurringBookingSchema = createAdminBookingSchema.extend({
|
||||
isRecurring: z.literal(true),
|
||||
recurringEndDate: z
|
||||
.string()
|
||||
.regex(ISO_DATE_REGEX, 'La fecha debe tener formato YYYY-MM-DD.')
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export const recurringBookingGroupSchema = z.object({
|
||||
id: z.uuid(),
|
||||
complexId: z.uuid(),
|
||||
courtId: z.uuid(),
|
||||
startTime: z.string().regex(TIME_REGEX),
|
||||
endTime: z.string().regex(TIME_REGEX),
|
||||
dayOfWeek: z.enum(['MONDAY', 'TUESDAY', 'WEDNESDAY', 'THURSDAY', 'FRIDAY', 'SATURDAY', 'SUNDAY']),
|
||||
startDate: z.string().regex(ISO_DATE_REGEX),
|
||||
endDate: z.string().regex(ISO_DATE_REGEX).nullable(),
|
||||
status: recurringGroupStatusSchema,
|
||||
customerName: z.string(),
|
||||
customerPhone: z.string(),
|
||||
customerEmail: z.string(),
|
||||
bookings: z.array(adminBookingSchema),
|
||||
createdAt: z.string().datetime(),
|
||||
updatedAt: z.string().datetime(),
|
||||
});
|
||||
|
||||
export const rescheduleAdminBookingSchema = z
|
||||
.object({
|
||||
courtId: z.string().uuid('El courtId debe ser un UUID válido.').optional(),
|
||||
startTime: z
|
||||
.string()
|
||||
.regex(TIME_REGEX, 'La hora debe tener formato HH:mm.')
|
||||
.optional(),
|
||||
})
|
||||
.refine(
|
||||
(data) => data.courtId !== undefined || data.startTime !== undefined,
|
||||
{ message: 'Debe proporcionar al menos una cancha o un horario diferente.' }
|
||||
);
|
||||
|
||||
export const updateRecurringGroupSchema = z.object({
|
||||
courtId: z.uuid('El courtId debe ser un UUID valido.').optional(),
|
||||
dayOfWeek: z
|
||||
.enum(['MONDAY', 'TUESDAY', 'WEDNESDAY', 'THURSDAY', 'FRIDAY', 'SATURDAY', 'SUNDAY'])
|
||||
.optional(),
|
||||
startTime: z.string().regex(TIME_REGEX, 'La hora debe tener formato HH:mm.').optional(),
|
||||
customerName: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(2, 'El nombre debe tener al menos 2 caracteres.')
|
||||
.max(120, 'El nombre no puede superar los 120 caracteres.')
|
||||
.optional(),
|
||||
customerPhone: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(6, 'El telefono debe tener al menos 6 caracteres.')
|
||||
.max(30, 'El telefono no puede superar los 30 caracteres.')
|
||||
.optional(),
|
||||
customerEmail: z.string().email('El email ingresado no es valido.').or(z.literal('')).optional(),
|
||||
});
|
||||
|
||||
export const listRecurringGroupsResponseSchema = z.object({
|
||||
groups: z.array(recurringBookingGroupSchema),
|
||||
});
|
||||
|
||||
export type BookingStatus = z.infer<typeof bookingStatusSchema>;
|
||||
export type RecurringGroupStatus = z.infer<typeof recurringGroupStatusSchema>;
|
||||
export type AdminBookingSport = z.infer<typeof adminBookingSportSchema>;
|
||||
export type AdminBooking = z.infer<typeof adminBookingSchema>;
|
||||
export type ListAdminBookingsQuery = z.infer<typeof listAdminBookingsQuerySchema>;
|
||||
export type ListAdminBookingsResponse = z.infer<typeof listAdminBookingsResponseSchema>;
|
||||
export type CreateAdminBookingInput = z.infer<typeof createAdminBookingSchema>;
|
||||
export type CreateRecurringBookingInput = z.infer<typeof createRecurringBookingSchema>;
|
||||
export type RecurringBookingGroup = z.infer<typeof recurringBookingGroupSchema>;
|
||||
export type UpdateAdminBookingStatusInput = z.infer<typeof updateAdminBookingStatusSchema>;
|
||||
export type RescheduleAdminBookingInput = z.infer<typeof rescheduleAdminBookingSchema>;
|
||||
export type UpdateRecurringGroupInput = z.infer<typeof updateRecurringGroupSchema>;
|
||||
export type ListRecurringGroupsResponse = z.infer<typeof listRecurringGroupsResponseSchema>;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { z } from 'zod'
|
||||
import { z } from 'zod';
|
||||
|
||||
export const adminPaymentStatusSchema = z.enum(['active', 'no_plan', 'expired'])
|
||||
export const adminPaymentStatusSchema = z.enum(['active', 'no_plan', 'expired']);
|
||||
|
||||
export const adminComplexListItemSchema = z.object({
|
||||
id: z.string(),
|
||||
@@ -13,7 +13,7 @@ export const adminComplexListItemSchema = z.object({
|
||||
courtCount: z.number().int().nonnegative(),
|
||||
avgBookingsPerDay: z.number().nonnegative(),
|
||||
paymentStatus: adminPaymentStatusSchema,
|
||||
})
|
||||
});
|
||||
|
||||
export const adminComplexStatsSchema = z.object({
|
||||
id: z.string(),
|
||||
@@ -34,20 +34,20 @@ export const adminComplexStatsSchema = z.object({
|
||||
noshow: z.number().int().nonnegative(),
|
||||
}),
|
||||
paymentStatus: adminPaymentStatusSchema,
|
||||
})
|
||||
});
|
||||
|
||||
export const adminCreatePlanSchema = z.object({
|
||||
code: z.string().trim().min(1).max(10),
|
||||
name: z.string().trim().min(1).max(30),
|
||||
price: z.number().nonnegative(),
|
||||
rules: z.any(),
|
||||
})
|
||||
});
|
||||
|
||||
export const adminUpdatePlanSchema = z.object({
|
||||
name: z.string().trim().min(1).max(30).optional(),
|
||||
price: z.number().nonnegative().optional(),
|
||||
rules: z.any().optional(),
|
||||
})
|
||||
});
|
||||
|
||||
export const adminUserSchema = z.object({
|
||||
id: z.string(),
|
||||
@@ -60,18 +60,18 @@ export const adminUserSchema = z.object({
|
||||
createdAt: z.string().datetime(),
|
||||
complexCount: z.number().int().nonnegative(),
|
||||
activeSessions: z.number().int().nonnegative(),
|
||||
})
|
||||
});
|
||||
|
||||
export const adminBlockUserSchema = z.object({
|
||||
banReason: z.string().max(500).optional(),
|
||||
})
|
||||
});
|
||||
|
||||
export const adminGlobalStatsSchema = z.object({
|
||||
totalComplexes: z.number().int().nonnegative(),
|
||||
totalUsers: z.number().int().nonnegative(),
|
||||
totalCourts: z.number().int().nonnegative(),
|
||||
totalBookings: z.number().int().nonnegative(),
|
||||
})
|
||||
});
|
||||
|
||||
export const adminUserSessionSchema = z.object({
|
||||
id: z.string(),
|
||||
@@ -82,35 +82,35 @@ export const adminUserSessionSchema = z.object({
|
||||
city: z.string().nullable(),
|
||||
country: z.string().nullable(),
|
||||
countryCode: z.string().nullable(),
|
||||
})
|
||||
});
|
||||
|
||||
export type AdminComplexListItem = z.infer<typeof adminComplexListItemSchema>
|
||||
export type AdminComplexStats = z.infer<typeof adminComplexStatsSchema>
|
||||
export type AdminCreatePlanInput = z.infer<typeof adminCreatePlanSchema>
|
||||
export type AdminUpdatePlanInput = z.infer<typeof adminUpdatePlanSchema>
|
||||
export type AdminUser = z.infer<typeof adminUserSchema>
|
||||
export type AdminBlockUserInput = z.infer<typeof adminBlockUserSchema>
|
||||
export type AdminGlobalStats = z.infer<typeof adminGlobalStatsSchema>
|
||||
export type AdminPaymentStatus = z.infer<typeof adminPaymentStatusSchema>
|
||||
export type AdminUserSession = z.infer<typeof adminUserSessionSchema>
|
||||
export type AdminComplexListItem = z.infer<typeof adminComplexListItemSchema>;
|
||||
export type AdminComplexStats = z.infer<typeof adminComplexStatsSchema>;
|
||||
export type AdminCreatePlanInput = z.infer<typeof adminCreatePlanSchema>;
|
||||
export type AdminUpdatePlanInput = z.infer<typeof adminUpdatePlanSchema>;
|
||||
export type AdminUser = z.infer<typeof adminUserSchema>;
|
||||
export type AdminBlockUserInput = z.infer<typeof adminBlockUserSchema>;
|
||||
export type AdminGlobalStats = z.infer<typeof adminGlobalStatsSchema>;
|
||||
export type AdminPaymentStatus = z.infer<typeof adminPaymentStatusSchema>;
|
||||
export type AdminUserSession = z.infer<typeof adminUserSessionSchema>;
|
||||
|
||||
export const adminGeoCitySchema = z.object({
|
||||
city: z.string(),
|
||||
userCount: z.number().int().nonnegative(),
|
||||
})
|
||||
});
|
||||
|
||||
export const adminGeoCountrySchema = z.object({
|
||||
country: z.string(),
|
||||
countryCode: z.string(),
|
||||
totalUsers: z.number().int().nonnegative(),
|
||||
cities: z.array(adminGeoCitySchema),
|
||||
})
|
||||
});
|
||||
|
||||
export const adminGeoStatsSchema = z.object({
|
||||
countries: z.array(adminGeoCountrySchema),
|
||||
totalUniqueCountries: z.number().int().nonnegative(),
|
||||
})
|
||||
});
|
||||
|
||||
export type AdminGeoCity = z.infer<typeof adminGeoCitySchema>
|
||||
export type AdminGeoCountry = z.infer<typeof adminGeoCountrySchema>
|
||||
export type AdminGeoStats = z.infer<typeof adminGeoStatsSchema>
|
||||
export type AdminGeoCity = z.infer<typeof adminGeoCitySchema>;
|
||||
export type AdminGeoCountry = z.infer<typeof adminGeoCountrySchema>;
|
||||
export type AdminGeoStats = z.infer<typeof adminGeoStatsSchema>;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { z } from 'zod'
|
||||
import { z } from 'zod';
|
||||
import { planFeatureFlagsSchema } from './plan';
|
||||
|
||||
const dayOfWeekEnum = z.enum([
|
||||
'MONDAY',
|
||||
@@ -8,7 +9,7 @@ const dayOfWeekEnum = z.enum([
|
||||
'FRIDAY',
|
||||
'SATURDAY',
|
||||
'SUNDAY',
|
||||
])
|
||||
]);
|
||||
|
||||
export const complexSchema = z.object({
|
||||
id: z.uuid(),
|
||||
@@ -22,7 +23,7 @@ export const complexSchema = z.object({
|
||||
planCode: z.string().max(10).nullable(),
|
||||
createdAt: z.string().datetime(),
|
||||
updatedAt: z.string().datetime(),
|
||||
})
|
||||
});
|
||||
|
||||
export const createComplexSchema = z
|
||||
.object({
|
||||
@@ -36,21 +37,13 @@ export const createComplexSchema = z
|
||||
.trim()
|
||||
.min(5, 'La direccion debe tener al menos 5 caracteres.')
|
||||
.max(200, 'La direccion no puede superar los 200 caracteres.'),
|
||||
city: z
|
||||
.string()
|
||||
.trim()
|
||||
.max(100, 'La ciudad no puede superar los 100 caracteres.')
|
||||
.optional(),
|
||||
city: z.string().trim().max(100, 'La ciudad no puede superar los 100 caracteres.').optional(),
|
||||
state: z
|
||||
.string()
|
||||
.trim()
|
||||
.max(100, 'La provincia/estado no puede superar los 100 caracteres.')
|
||||
.optional(),
|
||||
country: z
|
||||
.string()
|
||||
.trim()
|
||||
.max(100, 'El país no puede superar los 100 caracteres.')
|
||||
.optional(),
|
||||
country: z.string().trim().max(100, 'El país no puede superar los 100 caracteres.').optional(),
|
||||
planCode: z
|
||||
.string()
|
||||
.trim()
|
||||
@@ -70,31 +63,31 @@ export const createComplexSchema = z
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ['courtSportId'],
|
||||
message: 'Debes seleccionar un deporte.',
|
||||
})
|
||||
});
|
||||
}
|
||||
if (!data.courtStartTime || !/^\d{2}:\d{2}$/.test(data.courtStartTime)) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ['courtStartTime'],
|
||||
message: 'La hora de inicio debe tener formato HH:mm.',
|
||||
})
|
||||
});
|
||||
}
|
||||
if (!data.courtEndTime || !/^\d{2}:\d{2}$/.test(data.courtEndTime)) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ['courtEndTime'],
|
||||
message: 'La hora de fin debe tener formato HH:mm.',
|
||||
})
|
||||
});
|
||||
}
|
||||
if (!data.courtDaysOfWeek || data.courtDaysOfWeek.length === 0) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ['courtDaysOfWeek'],
|
||||
message: 'Debes seleccionar al menos un día.',
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
export const updateComplexSchema = z
|
||||
.object({
|
||||
@@ -154,16 +147,16 @@ export const updateComplexSchema = z
|
||||
payload.country !== undefined ||
|
||||
payload.complexSlug ||
|
||||
payload.adminEmail ||
|
||||
payload.planCode !== undefined,
|
||||
payload.planCode !== undefined
|
||||
),
|
||||
{
|
||||
message: 'Debes enviar al menos un campo para actualizar.',
|
||||
},
|
||||
)
|
||||
}
|
||||
);
|
||||
|
||||
export const complexUserRoleSchema = z.enum(['ADMIN', 'EMPLOYEE'])
|
||||
export const complexUserRoleSchema = z.enum(['ADMIN', 'EMPLOYEE']);
|
||||
|
||||
export type ComplexUserRole = z.infer<typeof complexUserRoleSchema>
|
||||
export type ComplexUserRole = z.infer<typeof complexUserRoleSchema>;
|
||||
|
||||
export const complexMemberSchema = z.object({
|
||||
userId: z.string(),
|
||||
@@ -172,13 +165,13 @@ export const complexMemberSchema = z.object({
|
||||
avatarUrl: z.url(),
|
||||
role: complexUserRoleSchema,
|
||||
createdAt: z.string().datetime(),
|
||||
})
|
||||
});
|
||||
|
||||
export type ComplexMember = z.infer<typeof complexMemberSchema>
|
||||
export type ComplexMember = z.infer<typeof complexMemberSchema>;
|
||||
|
||||
export const complexInvitationStatusSchema = z.enum(['PENDING', 'ACCEPTED', 'REVOKED', 'EXPIRED'])
|
||||
export const complexInvitationStatusSchema = z.enum(['PENDING', 'ACCEPTED', 'REVOKED', 'EXPIRED']);
|
||||
|
||||
export type ComplexInvitationStatus = z.infer<typeof complexInvitationStatusSchema>
|
||||
export type ComplexInvitationStatus = z.infer<typeof complexInvitationStatusSchema>;
|
||||
|
||||
export const complexInvitationSchema = z.object({
|
||||
id: z.uuid(),
|
||||
@@ -189,62 +182,63 @@ export const complexInvitationSchema = z.object({
|
||||
expiresAt: z.string().datetime(),
|
||||
acceptedAt: z.string().datetime().nullable(),
|
||||
revokedAt: z.string().datetime().nullable(),
|
||||
})
|
||||
});
|
||||
|
||||
export type ComplexInvitation = z.infer<typeof complexInvitationSchema>
|
||||
export type ComplexInvitation = z.infer<typeof complexInvitationSchema>;
|
||||
|
||||
export const complexUsersOverviewSchema = z.object({
|
||||
members: z.array(complexMemberSchema),
|
||||
invitations: z.array(complexInvitationSchema),
|
||||
})
|
||||
});
|
||||
|
||||
export type ComplexUsersOverview = z.infer<typeof complexUsersOverviewSchema>
|
||||
export type ComplexUsersOverview = z.infer<typeof complexUsersOverviewSchema>;
|
||||
|
||||
export const inviteComplexUserSchema = z.object({
|
||||
email: z.email('El email debe ser válido.'),
|
||||
})
|
||||
});
|
||||
|
||||
export type InviteComplexUserInput = z.infer<typeof inviteComplexUserSchema>
|
||||
export type InviteComplexUserInput = z.infer<typeof inviteComplexUserSchema>;
|
||||
|
||||
export const acceptComplexInvitationsSchema = z.object({
|
||||
inviteToken: z.string().min(1, 'El token de invitación no puede estar vacío.').optional(),
|
||||
})
|
||||
});
|
||||
|
||||
export type AcceptComplexInvitationsInput = z.infer<typeof acceptComplexInvitationsSchema>
|
||||
export type AcceptComplexInvitationsInput = z.infer<typeof acceptComplexInvitationsSchema>;
|
||||
|
||||
export const revokeComplexUserSchema = z.object({
|
||||
userId: z.string().min(1, 'El userId no puede estar vacío.'),
|
||||
})
|
||||
});
|
||||
|
||||
export type RevokeComplexUserInput = z.infer<typeof revokeComplexUserSchema>
|
||||
export type RevokeComplexUserInput = z.infer<typeof revokeComplexUserSchema>;
|
||||
|
||||
export const selectComplexSchema = z.object({
|
||||
complexId: z.string().uuid(),
|
||||
})
|
||||
});
|
||||
|
||||
export type SelectComplexInput = z.infer<typeof selectComplexSchema>
|
||||
export type SelectComplexInput = z.infer<typeof selectComplexSchema>;
|
||||
|
||||
export const complexWithRoleSchema = complexSchema.merge(
|
||||
z.object({
|
||||
role: complexUserRoleSchema,
|
||||
planFeatures: planFeatureFlagsSchema.nullable(),
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
export type ComplexWithRole = z.infer<typeof complexWithRoleSchema>
|
||||
export type ComplexWithRole = z.infer<typeof complexWithRoleSchema>;
|
||||
|
||||
export type Complex = z.infer<typeof complexSchema>
|
||||
export type CreateComplexInput = z.infer<typeof createComplexSchema>
|
||||
export type UpdateComplexInput = z.infer<typeof updateComplexSchema>
|
||||
export type CreateComplexPayload = CreateComplexInput
|
||||
export type CreateComplexResponse = Complex
|
||||
export type UpdateComplexPayload = UpdateComplexInput
|
||||
export type UpdateComplexResponse = Complex
|
||||
export type Complex = z.infer<typeof complexSchema>;
|
||||
export type CreateComplexInput = z.infer<typeof createComplexSchema>;
|
||||
export type UpdateComplexInput = z.infer<typeof updateComplexSchema>;
|
||||
export type CreateComplexPayload = CreateComplexInput;
|
||||
export type CreateComplexResponse = Complex;
|
||||
export type UpdateComplexPayload = UpdateComplexInput;
|
||||
export type UpdateComplexResponse = Complex;
|
||||
export type InviteComplexUserResponse = {
|
||||
invitation: ComplexInvitation
|
||||
}
|
||||
invitation: ComplexInvitation;
|
||||
};
|
||||
export type AcceptComplexInvitationsResponse = {
|
||||
acceptedCount: number
|
||||
}
|
||||
acceptedCount: number;
|
||||
};
|
||||
export type CancelComplexInvitationResponse = {
|
||||
ok: boolean
|
||||
}
|
||||
ok: boolean;
|
||||
};
|
||||
|
||||
@@ -1,65 +1,65 @@
|
||||
import { z } from 'zod'
|
||||
import { z } from 'zod';
|
||||
|
||||
const TIME_REGEX = /^([01]\d|2[0-3]):([0-5]\d)$/
|
||||
const TIME_REGEX = /^([01]\d|2[0-3]):([0-5]\d)$/;
|
||||
|
||||
function toMinutes(value: string): number {
|
||||
const [hours, minutes] = value.split(':').map((part) => Number(part))
|
||||
return hours * 60 + minutes
|
||||
const [hours, minutes] = value.split(':').map((part) => Number(part));
|
||||
return hours * 60 + minutes;
|
||||
}
|
||||
|
||||
function hasOverlaps(
|
||||
availability: Array<{
|
||||
dayOfWeek: DayOfWeek
|
||||
startTime: string
|
||||
endTime: string
|
||||
}>,
|
||||
dayOfWeek: DayOfWeek;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
}>
|
||||
): boolean {
|
||||
const grouped = new Map<
|
||||
DayOfWeek,
|
||||
Array<{
|
||||
start: number
|
||||
end: number
|
||||
start: number;
|
||||
end: number;
|
||||
}>
|
||||
>()
|
||||
>();
|
||||
|
||||
for (const range of availability) {
|
||||
const start = toMinutes(range.startTime)
|
||||
const end = toMinutes(range.endTime)
|
||||
const current = grouped.get(range.dayOfWeek) ?? []
|
||||
current.push({ start, end })
|
||||
grouped.set(range.dayOfWeek, current)
|
||||
const start = toMinutes(range.startTime);
|
||||
const end = toMinutes(range.endTime);
|
||||
const current = grouped.get(range.dayOfWeek) ?? [];
|
||||
current.push({ start, end });
|
||||
grouped.set(range.dayOfWeek, current);
|
||||
}
|
||||
|
||||
for (const ranges of grouped.values()) {
|
||||
ranges.sort((a, b) => a.start - b.start)
|
||||
ranges.sort((a, b) => a.start - b.start);
|
||||
|
||||
for (let index = 1; index < ranges.length; index += 1) {
|
||||
const previous = ranges[index - 1]
|
||||
const current = ranges[index]
|
||||
const previous = ranges[index - 1];
|
||||
const current = ranges[index];
|
||||
|
||||
if (previous.end > current.start) {
|
||||
return true
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
|
||||
function refineAvailability(
|
||||
availability: Array<{
|
||||
dayOfWeek: DayOfWeek
|
||||
startTime: string
|
||||
endTime: string
|
||||
dayOfWeek: DayOfWeek;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
}>,
|
||||
context: z.RefinementCtx,
|
||||
context: z.RefinementCtx
|
||||
) {
|
||||
for (const range of availability) {
|
||||
if (toMinutes(range.startTime) >= toMinutes(range.endTime)) {
|
||||
context.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: `El rango ${range.startTime}-${range.endTime} es invalido. La hora de inicio debe ser menor a la de fin.`,
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ function refineAvailability(
|
||||
context.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'Hay rangos horarios superpuestos para el mismo dia.',
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,22 +79,22 @@ export const dayOfWeekSchema = z.enum([
|
||||
'FRIDAY',
|
||||
'SATURDAY',
|
||||
'SUNDAY',
|
||||
])
|
||||
]);
|
||||
|
||||
export type DayOfWeek = z.infer<typeof dayOfWeekSchema>
|
||||
export type DayOfWeek = z.infer<typeof dayOfWeekSchema>;
|
||||
|
||||
export const courtAvailabilitySchema = z.object({
|
||||
id: z.uuid(),
|
||||
dayOfWeek: dayOfWeekSchema,
|
||||
startTime: z.string().regex(TIME_REGEX, 'La hora debe tener formato HH:mm.'),
|
||||
endTime: z.string().regex(TIME_REGEX, 'La hora debe tener formato HH:mm.'),
|
||||
})
|
||||
});
|
||||
|
||||
export const courtAvailabilityInputSchema = z.object({
|
||||
dayOfWeek: dayOfWeekSchema,
|
||||
startTime: z.string().regex(TIME_REGEX, 'La hora debe tener formato HH:mm.'),
|
||||
endTime: z.string().regex(TIME_REGEX, 'La hora debe tener formato HH:mm.'),
|
||||
})
|
||||
});
|
||||
|
||||
export const courtPriceRuleSchema = z.object({
|
||||
id: z.uuid(),
|
||||
@@ -103,18 +103,19 @@ export const courtPriceRuleSchema = z.object({
|
||||
endTime: z.string().nullable(),
|
||||
price: z.number().nonnegative(),
|
||||
isActive: z.boolean(),
|
||||
})
|
||||
});
|
||||
|
||||
export const courtSportSchema = z.object({
|
||||
id: z.uuid(),
|
||||
name: z.string(),
|
||||
slug: z.string(),
|
||||
})
|
||||
});
|
||||
|
||||
export const courtSchema = z.object({
|
||||
id: z.uuid(),
|
||||
complexId: z.uuid(),
|
||||
name: z.string(),
|
||||
isUnderMaintenance: z.boolean(),
|
||||
sportId: z.uuid(),
|
||||
sport: courtSportSchema,
|
||||
slotDurationMinutes: z.int().positive(),
|
||||
@@ -124,7 +125,7 @@ export const courtSchema = z.object({
|
||||
hasCustomPricing: z.boolean(),
|
||||
createdAt: z.string().datetime(),
|
||||
updatedAt: z.string().datetime(),
|
||||
})
|
||||
});
|
||||
|
||||
export const createCourtSchema = z
|
||||
.object({
|
||||
@@ -139,9 +140,11 @@ export const createCourtSchema = z
|
||||
.min(15, 'La duracion minima por turno es 15 minutos.')
|
||||
.max(480, 'La duracion maxima por turno es 480 minutos.'),
|
||||
basePrice: z.number().nonnegative('El precio base no puede ser negativo.'),
|
||||
availability: z.array(courtAvailabilityInputSchema).min(1, 'Debes definir al menos un rango horario.'),
|
||||
availability: z
|
||||
.array(courtAvailabilityInputSchema)
|
||||
.min(1, 'Debes definir al menos un rango horario.'),
|
||||
})
|
||||
.superRefine((payload, context) => refineAvailability(payload.availability, context))
|
||||
.superRefine((payload, context) => refineAvailability(payload.availability, context));
|
||||
|
||||
export const updateCourtSchema = z
|
||||
.object({
|
||||
@@ -158,7 +161,10 @@ export const updateCourtSchema = z
|
||||
.max(480, 'La duracion maxima por turno es 480 minutos.')
|
||||
.optional(),
|
||||
basePrice: z.number().nonnegative('El precio base no puede ser negativo.').optional(),
|
||||
availability: z.array(courtAvailabilityInputSchema).min(1, 'Debes definir al menos un rango horario.').optional(),
|
||||
availability: z
|
||||
.array(courtAvailabilityInputSchema)
|
||||
.min(1, 'Debes definir al menos un rango horario.')
|
||||
.optional(),
|
||||
})
|
||||
.superRefine((payload, context) => {
|
||||
if (
|
||||
@@ -171,17 +177,17 @@ export const updateCourtSchema = z
|
||||
context.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'Debes enviar al menos un campo para actualizar.',
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
if (payload.availability) {
|
||||
refineAvailability(payload.availability, context)
|
||||
refineAvailability(payload.availability, context);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
export type Court = z.infer<typeof courtSchema>
|
||||
export type CourtAvailability = z.infer<typeof courtAvailabilitySchema>
|
||||
export type CourtAvailabilityInput = z.infer<typeof courtAvailabilityInputSchema>
|
||||
export type CourtPriceRule = z.infer<typeof courtPriceRuleSchema>
|
||||
export type CreateCourtInput = z.infer<typeof createCourtSchema>
|
||||
export type UpdateCourtInput = z.infer<typeof updateCourtSchema>
|
||||
export type Court = z.infer<typeof courtSchema>;
|
||||
export type CourtAvailability = z.infer<typeof courtAvailabilitySchema>;
|
||||
export type CourtAvailabilityInput = z.infer<typeof courtAvailabilityInputSchema>;
|
||||
export type CourtPriceRule = z.infer<typeof courtPriceRuleSchema>;
|
||||
export type CreateCourtInput = z.infer<typeof createCourtSchema>;
|
||||
export type UpdateCourtInput = z.infer<typeof updateCourtSchema>;
|
||||
|
||||
@@ -6,7 +6,7 @@ export {
|
||||
planSummarySchema,
|
||||
planWithFeaturesSchema,
|
||||
planLimitsSchema,
|
||||
} from './plan'
|
||||
} from './plan';
|
||||
export type {
|
||||
Plan,
|
||||
PlanFeatureFlags,
|
||||
@@ -15,7 +15,7 @@ export type {
|
||||
PlanSummary,
|
||||
PlanWithFeatures,
|
||||
PlanLimits,
|
||||
} from './plan'
|
||||
} from './plan';
|
||||
export {
|
||||
acceptComplexInvitationsSchema,
|
||||
complexInvitationSchema,
|
||||
@@ -30,7 +30,7 @@ export {
|
||||
revokeComplexUserSchema,
|
||||
selectComplexSchema,
|
||||
updateComplexSchema,
|
||||
} from './complex'
|
||||
} from './complex';
|
||||
export type {
|
||||
AcceptComplexInvitationsInput,
|
||||
AcceptComplexInvitationsResponse,
|
||||
@@ -52,7 +52,7 @@ export type {
|
||||
UpdateComplexPayload,
|
||||
UpdateComplexResponse,
|
||||
RevokeComplexUserInput,
|
||||
} from './complex'
|
||||
} from './complex';
|
||||
export {
|
||||
dayOfWeekSchema,
|
||||
courtAvailabilityInputSchema,
|
||||
@@ -61,7 +61,7 @@ export {
|
||||
courtSchema,
|
||||
createCourtSchema,
|
||||
updateCourtSchema,
|
||||
} from './court'
|
||||
} from './court';
|
||||
export type {
|
||||
DayOfWeek,
|
||||
Court,
|
||||
@@ -70,25 +70,37 @@ export type {
|
||||
CourtPriceRule,
|
||||
CreateCourtInput,
|
||||
UpdateCourtInput,
|
||||
} from './court'
|
||||
} from './court';
|
||||
export {
|
||||
adminBookingSchema,
|
||||
adminBookingSportSchema,
|
||||
bookingStatusSchema,
|
||||
createAdminBookingSchema,
|
||||
createRecurringBookingSchema,
|
||||
listAdminBookingsQuerySchema,
|
||||
listAdminBookingsResponseSchema,
|
||||
listRecurringGroupsResponseSchema,
|
||||
recurringBookingGroupSchema,
|
||||
recurringGroupStatusSchema,
|
||||
rescheduleAdminBookingSchema,
|
||||
updateAdminBookingStatusSchema,
|
||||
} from './admin-booking'
|
||||
updateRecurringGroupSchema,
|
||||
} from './admin-booking';
|
||||
export type {
|
||||
AdminBooking,
|
||||
AdminBookingSport,
|
||||
BookingStatus,
|
||||
CreateAdminBookingInput,
|
||||
CreateRecurringBookingInput,
|
||||
ListAdminBookingsQuery,
|
||||
ListAdminBookingsResponse,
|
||||
ListRecurringGroupsResponse,
|
||||
RecurringBookingGroup,
|
||||
RecurringGroupStatus,
|
||||
RescheduleAdminBookingInput,
|
||||
UpdateAdminBookingStatusInput,
|
||||
} from './admin-booking'
|
||||
UpdateRecurringGroupInput,
|
||||
} from './admin-booking';
|
||||
export {
|
||||
createPublicBookingSchema,
|
||||
publicAvailabilityCourtSchema,
|
||||
@@ -99,7 +111,7 @@ export {
|
||||
publicBookingSlotSchema,
|
||||
publicBookingSportSchema,
|
||||
cancelPublicBookingSchema,
|
||||
} from './public-booking'
|
||||
} from './public-booking';
|
||||
export type {
|
||||
CreatePublicBookingInput,
|
||||
CancelPublicBookingInput,
|
||||
@@ -110,15 +122,15 @@ export type {
|
||||
PublicBookingConfirmation,
|
||||
PublicBookingSlot,
|
||||
PublicBookingSport,
|
||||
} from './public-booking'
|
||||
export { sportSchema, createSportSchema, updateSportSchema } from './sport'
|
||||
export type { Sport, CreateSportInput, UpdateSportInput } from './sport'
|
||||
} from './public-booking';
|
||||
export { sportSchema, createSportSchema, updateSportSchema } from './sport';
|
||||
export type { Sport, CreateSportInput, UpdateSportInput } from './sport';
|
||||
export {
|
||||
userProfileSchema,
|
||||
passwordResetStartSchema,
|
||||
passwordResetVerifySchema,
|
||||
passwordResetResendSchema,
|
||||
} from './user'
|
||||
} from './user';
|
||||
export type {
|
||||
UserProfile,
|
||||
UserProfileResponse,
|
||||
@@ -128,7 +140,7 @@ export type {
|
||||
PasswordResetStartResponse,
|
||||
PasswordResetVerifyResponse,
|
||||
PasswordResetResendResponse,
|
||||
} from './user'
|
||||
} from './user';
|
||||
export {
|
||||
adminComplexListItemSchema,
|
||||
adminComplexStatsSchema,
|
||||
@@ -142,7 +154,7 @@ export {
|
||||
adminGeoCitySchema,
|
||||
adminGeoCountrySchema,
|
||||
adminGeoStatsSchema,
|
||||
} from './admin'
|
||||
} from './admin';
|
||||
export type {
|
||||
AdminComplexListItem,
|
||||
AdminComplexStats,
|
||||
@@ -156,4 +168,4 @@ export type {
|
||||
AdminGeoCity,
|
||||
AdminGeoCountry,
|
||||
AdminGeoStats,
|
||||
} from './admin'
|
||||
} from './admin';
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { z } from 'zod'
|
||||
import { z } from 'zod';
|
||||
|
||||
const nonNegativeInt = z.int().nonnegative()
|
||||
const positiveInt = z.int().positive()
|
||||
const nonNegativeInt = z.int().nonnegative();
|
||||
const positiveInt = z.int().positive();
|
||||
|
||||
export const planFeatureFlagsSchema = z.object({
|
||||
onlinePayments: z.boolean().default(false),
|
||||
publicBookingPage: z.boolean().default(false),
|
||||
advancedReports: z.boolean().default(false),
|
||||
whatsappReminders: z.boolean().default(false),
|
||||
})
|
||||
fixedSlots: z.boolean().default(false),
|
||||
});
|
||||
|
||||
export const planRulesV1Schema = z.object({
|
||||
version: z.literal('v1'),
|
||||
@@ -31,13 +32,14 @@ export const planRulesV1Schema = z.object({
|
||||
publicBookingPage: false,
|
||||
advancedReports: false,
|
||||
whatsappReminders: false,
|
||||
fixedSlots: false,
|
||||
}),
|
||||
})
|
||||
});
|
||||
|
||||
export const planPriceOverrideSchema = z.object({
|
||||
amount: z.number().nonnegative(),
|
||||
currency: z.string().length(3),
|
||||
})
|
||||
});
|
||||
|
||||
export const planRulesV2Schema = planRulesV1Schema.extend({
|
||||
version: z.literal('v2'),
|
||||
@@ -46,12 +48,12 @@ export const planRulesV2Schema = planRulesV1Schema.extend({
|
||||
overrides: z.record(z.string(), planPriceOverrideSchema),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
});
|
||||
|
||||
export const planRulesSchema = z.discriminatedUnion('version', [
|
||||
planRulesV1Schema,
|
||||
planRulesV2Schema,
|
||||
])
|
||||
]);
|
||||
|
||||
export const planSchema = z.object({
|
||||
code: z.string().trim().min(1).max(10),
|
||||
@@ -59,13 +61,13 @@ export const planSchema = z.object({
|
||||
price: z.number().nonnegative(),
|
||||
lastUpdatedAt: z.string().datetime(),
|
||||
rules: planRulesSchema,
|
||||
})
|
||||
});
|
||||
|
||||
export const planSummarySchema = z.object({
|
||||
code: z.string().trim().min(1).max(10),
|
||||
name: z.string().trim().min(1).max(30),
|
||||
price: z.number().nonnegative(),
|
||||
})
|
||||
});
|
||||
|
||||
export const planLimitsSchema = z.object({
|
||||
maxCourts: positiveInt,
|
||||
@@ -73,7 +75,7 @@ export const planLimitsSchema = z.object({
|
||||
maxActiveUsers: positiveInt.optional(),
|
||||
maxConcurrentBookingsPerSlot: positiveInt.optional(),
|
||||
maxSports: positiveInt.optional(),
|
||||
})
|
||||
});
|
||||
|
||||
export const planWithFeaturesSchema = z.object({
|
||||
code: z.string().trim().min(1).max(10),
|
||||
@@ -82,13 +84,13 @@ export const planWithFeaturesSchema = z.object({
|
||||
currency: z.string().length(3),
|
||||
features: planFeatureFlagsSchema,
|
||||
limits: planLimitsSchema,
|
||||
})
|
||||
});
|
||||
|
||||
export type PlanFeatureFlags = z.infer<typeof planFeatureFlagsSchema>
|
||||
export type PlanRulesV1 = z.infer<typeof planRulesV1Schema>
|
||||
export type PlanRulesV2 = z.infer<typeof planRulesV2Schema>
|
||||
export type PlanRules = z.infer<typeof planRulesSchema>
|
||||
export type Plan = z.infer<typeof planSchema>
|
||||
export type PlanSummary = z.infer<typeof planSummarySchema>
|
||||
export type PlanWithFeatures = z.infer<typeof planWithFeaturesSchema>
|
||||
export type PlanLimits = z.infer<typeof planLimitsSchema>
|
||||
export type PlanFeatureFlags = z.infer<typeof planFeatureFlagsSchema>;
|
||||
export type PlanRulesV1 = z.infer<typeof planRulesV1Schema>;
|
||||
export type PlanRulesV2 = z.infer<typeof planRulesV2Schema>;
|
||||
export type PlanRules = z.infer<typeof planRulesSchema>;
|
||||
export type Plan = z.infer<typeof planSchema>;
|
||||
export type PlanSummary = z.infer<typeof planSummarySchema>;
|
||||
export type PlanWithFeatures = z.infer<typeof planWithFeaturesSchema>;
|
||||
export type PlanLimits = z.infer<typeof planLimitsSchema>;
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
import { z } from 'zod'
|
||||
import { dayOfWeekSchema } from './court'
|
||||
import { z } from 'zod';
|
||||
import { dayOfWeekSchema } from './court';
|
||||
|
||||
const TIME_REGEX = /^([01]\d|2[0-3]):([0-5]\d)$/
|
||||
const ISO_DATE_REGEX = /^\d{4}-\d{2}-\d{2}$/
|
||||
const BOOKING_CODE_REGEX = /^[A-Z0-9]{6,8}$/
|
||||
const TIME_REGEX = /^([01]\d|2[0-3]):([0-5]\d)$/;
|
||||
const ISO_DATE_REGEX = /^\d{4}-\d{2}-\d{2}$/;
|
||||
const BOOKING_CODE_REGEX = /^[A-Z0-9]{6,8}$/;
|
||||
|
||||
export const publicAvailabilityQuerySchema = z.object({
|
||||
date: z.string().regex(ISO_DATE_REGEX, 'La fecha debe tener formato YYYY-MM-DD.'),
|
||||
sportId: z.uuid('El sportId debe ser un UUID valido.').optional(),
|
||||
})
|
||||
});
|
||||
|
||||
export const publicBookingSportSchema = z.object({
|
||||
id: z.uuid(),
|
||||
name: z.string(),
|
||||
slug: z.string(),
|
||||
})
|
||||
});
|
||||
|
||||
export const publicBookingSlotSchema = z.object({
|
||||
startTime: z.string().regex(TIME_REGEX),
|
||||
endTime: z.string().regex(TIME_REGEX),
|
||||
price: z.number().nonnegative(),
|
||||
})
|
||||
});
|
||||
|
||||
export const publicAvailabilityCourtSchema = z.object({
|
||||
courtId: z.uuid(),
|
||||
@@ -29,7 +29,7 @@ export const publicAvailabilityCourtSchema = z.object({
|
||||
slotDurationMinutes: z.int().positive(),
|
||||
availabilityDay: dayOfWeekSchema,
|
||||
availableSlots: z.array(publicBookingSlotSchema),
|
||||
})
|
||||
});
|
||||
|
||||
export const publicAvailabilityResponseSchema = z.object({
|
||||
complexId: z.uuid(),
|
||||
@@ -40,7 +40,7 @@ export const publicAvailabilityResponseSchema = z.object({
|
||||
sportSelectionRequired: z.boolean(),
|
||||
sports: z.array(publicBookingSportSchema),
|
||||
courts: z.array(publicAvailabilityCourtSchema),
|
||||
})
|
||||
});
|
||||
|
||||
export const createPublicBookingSchema = z.object({
|
||||
date: z.string().regex(ISO_DATE_REGEX, 'La fecha debe tener formato YYYY-MM-DD.'),
|
||||
@@ -57,10 +57,8 @@ export const createPublicBookingSchema = z.object({
|
||||
.trim()
|
||||
.min(6, 'El telefono debe tener al menos 6 caracteres.')
|
||||
.max(30, 'El telefono no puede superar los 30 caracteres.'),
|
||||
customerEmail: z
|
||||
.string()
|
||||
.email('El email ingresado no es valido.'),
|
||||
})
|
||||
customerEmail: z.string().email('El email ingresado no es valido.'),
|
||||
});
|
||||
|
||||
export const cancelPublicBookingSchema = z.object({
|
||||
bookingCode: z.string().regex(BOOKING_CODE_REGEX, 'El codigo de reserva no es valido.'),
|
||||
@@ -69,7 +67,7 @@ export const cancelPublicBookingSchema = z.object({
|
||||
.trim()
|
||||
.min(6, 'El telefono debe tener al menos 6 caracteres.')
|
||||
.max(30, 'El telefono no puede superar los 30 caracteres.'),
|
||||
})
|
||||
});
|
||||
|
||||
export const publicBookingSchema = z.object({
|
||||
id: z.uuid(),
|
||||
@@ -89,7 +87,7 @@ export const publicBookingSchema = z.object({
|
||||
status: z.enum(['CONFIRMED', 'CANCELLED', 'COMPLETED']),
|
||||
price: z.number().nonnegative(),
|
||||
createdAt: z.string().datetime(),
|
||||
})
|
||||
});
|
||||
|
||||
export const publicBookingConfirmationSchema = z.object({
|
||||
bookingCode: z.string().regex(BOOKING_CODE_REGEX),
|
||||
@@ -105,14 +103,14 @@ export const publicBookingConfirmationSchema = z.object({
|
||||
status: z.enum(['CONFIRMED', 'CANCELLED', 'COMPLETED']),
|
||||
customerEmail: z.string(),
|
||||
createdAt: z.string().datetime(),
|
||||
})
|
||||
});
|
||||
|
||||
export type PublicAvailabilityQuery = z.infer<typeof publicAvailabilityQuerySchema>
|
||||
export type PublicBookingSport = z.infer<typeof publicBookingSportSchema>
|
||||
export type PublicBookingSlot = z.infer<typeof publicBookingSlotSchema>
|
||||
export type PublicAvailabilityCourt = z.infer<typeof publicAvailabilityCourtSchema>
|
||||
export type PublicAvailabilityResponse = z.infer<typeof publicAvailabilityResponseSchema>
|
||||
export type CreatePublicBookingInput = z.infer<typeof createPublicBookingSchema>
|
||||
export type CancelPublicBookingInput = z.infer<typeof cancelPublicBookingSchema>
|
||||
export type PublicBooking = z.infer<typeof publicBookingSchema>
|
||||
export type PublicBookingConfirmation = z.infer<typeof publicBookingConfirmationSchema>
|
||||
export type PublicAvailabilityQuery = z.infer<typeof publicAvailabilityQuerySchema>;
|
||||
export type PublicBookingSport = z.infer<typeof publicBookingSportSchema>;
|
||||
export type PublicBookingSlot = z.infer<typeof publicBookingSlotSchema>;
|
||||
export type PublicAvailabilityCourt = z.infer<typeof publicAvailabilityCourtSchema>;
|
||||
export type PublicAvailabilityResponse = z.infer<typeof publicAvailabilityResponseSchema>;
|
||||
export type CreatePublicBookingInput = z.infer<typeof createPublicBookingSchema>;
|
||||
export type CancelPublicBookingInput = z.infer<typeof cancelPublicBookingSchema>;
|
||||
export type PublicBooking = z.infer<typeof publicBookingSchema>;
|
||||
export type PublicBookingConfirmation = z.infer<typeof publicBookingConfirmationSchema>;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { z } from 'zod'
|
||||
import { z } from 'zod';
|
||||
|
||||
export const sportSchema = z.object({
|
||||
id: z.uuid(),
|
||||
@@ -7,7 +7,7 @@ export const sportSchema = z.object({
|
||||
isActive: z.boolean(),
|
||||
createdAt: z.string().datetime(),
|
||||
updatedAt: z.string().datetime(),
|
||||
})
|
||||
});
|
||||
|
||||
export const createSportSchema = z.object({
|
||||
name: z
|
||||
@@ -15,7 +15,7 @@ export const createSportSchema = z.object({
|
||||
.trim()
|
||||
.min(2, 'El deporte debe tener al menos 2 caracteres.')
|
||||
.max(80, 'El deporte no puede superar los 80 caracteres.'),
|
||||
})
|
||||
});
|
||||
|
||||
export const updateSportSchema = z
|
||||
.object({
|
||||
@@ -29,8 +29,8 @@ export const updateSportSchema = z
|
||||
})
|
||||
.refine((payload) => payload.name !== undefined || payload.isActive !== undefined, {
|
||||
message: 'Debes enviar al menos un campo para actualizar.',
|
||||
})
|
||||
});
|
||||
|
||||
export type Sport = z.infer<typeof sportSchema>
|
||||
export type CreateSportInput = z.infer<typeof createSportSchema>
|
||||
export type UpdateSportInput = z.infer<typeof updateSportSchema>
|
||||
export type Sport = z.infer<typeof sportSchema>;
|
||||
export type CreateSportInput = z.infer<typeof createSportSchema>;
|
||||
export type UpdateSportInput = z.infer<typeof updateSportSchema>;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { z } from 'zod'
|
||||
import { z } from 'zod';
|
||||
|
||||
export const userProfileSchema = z.object({
|
||||
id: z.string(),
|
||||
@@ -7,30 +7,30 @@ export const userProfileSchema = z.object({
|
||||
role: z.string(),
|
||||
avatarUrl: z.url().nullable(),
|
||||
createdAt: z.string(),
|
||||
})
|
||||
});
|
||||
|
||||
export type UserProfile = z.infer<typeof userProfileSchema>
|
||||
export type UserProfileResponse = UserProfile
|
||||
export type UserProfile = z.infer<typeof userProfileSchema>;
|
||||
export type UserProfileResponse = UserProfile;
|
||||
|
||||
export const passwordResetStartSchema = z.object({
|
||||
email: z.string().email('Ingresá un email válido.'),
|
||||
})
|
||||
});
|
||||
|
||||
export type PasswordResetStartInput = z.infer<typeof passwordResetStartSchema>
|
||||
export type PasswordResetStartInput = z.infer<typeof passwordResetStartSchema>;
|
||||
|
||||
export const passwordResetVerifySchema = z.object({
|
||||
requestId: z.string().uuid('Request ID inválido.'),
|
||||
otp: z.string().length(6, 'El código debe tener 6 dígitos.'),
|
||||
newPassword: z.string().min(6, 'La contraseña debe tener al menos 6 caracteres.'),
|
||||
})
|
||||
});
|
||||
|
||||
export type PasswordResetVerifyInput = z.infer<typeof passwordResetVerifySchema>
|
||||
export type PasswordResetVerifyInput = z.infer<typeof passwordResetVerifySchema>;
|
||||
|
||||
export const passwordResetResendSchema = z.object({
|
||||
requestId: z.string().uuid('Request ID inválido.'),
|
||||
})
|
||||
});
|
||||
|
||||
export type PasswordResetResendInput = z.infer<typeof passwordResetResendSchema>
|
||||
export type PasswordResetResendInput = z.infer<typeof passwordResetResendSchema>;
|
||||
|
||||
export type PasswordResetStartResponse = {
|
||||
message: string;
|
||||
@@ -38,11 +38,11 @@ export type PasswordResetStartResponse = {
|
||||
email: string;
|
||||
expiresAt: string;
|
||||
cooldownSeconds: number;
|
||||
}
|
||||
};
|
||||
|
||||
export type PasswordResetVerifyResponse = {
|
||||
message: string;
|
||||
}
|
||||
};
|
||||
|
||||
export type PasswordResetResendResponse = {
|
||||
message: string;
|
||||
@@ -50,4 +50,4 @@ export type PasswordResetResendResponse = {
|
||||
email: string;
|
||||
expiresAt: string;
|
||||
cooldownSeconds: number;
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user