Compare commits
9 Commits
feat/admin
...
c7e685ea08
| Author | SHA1 | Date | |
|---|---|---|---|
| c7e685ea08 | |||
|
|
c8477de5d2 | ||
| 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`
|
2. Implement handler in `apps/backend`
|
||||||
3. Consume from `apps/frontend` via workspace import `@repo/api-contract`
|
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)
|
## Authentication (Better Auth)
|
||||||
|
|
||||||
The project uses **Better Auth** for session management, replacing the legacy Supabase Auth.
|
The project uses **Better Auth** for session management, replacing the legacy Supabase Auth.
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ model Complex {
|
|||||||
users ComplexUser[]
|
users ComplexUser[]
|
||||||
invitations ComplexInvitation[]
|
invitations ComplexInvitation[]
|
||||||
courts Court[]
|
courts Court[]
|
||||||
|
recurringGroups RecurringBookingGroup[]
|
||||||
|
|
||||||
@@index([planCode])
|
@@index([planCode])
|
||||||
@@index([complexSlug])
|
@@index([complexSlug])
|
||||||
|
|||||||
@@ -15,6 +15,11 @@ enum CourtBookingStatus {
|
|||||||
NOSHOW
|
NOSHOW
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum RecurringBookingGroupStatus {
|
||||||
|
ACTIVE
|
||||||
|
CANCELLED
|
||||||
|
}
|
||||||
|
|
||||||
model Sport {
|
model Sport {
|
||||||
id String @id @db.Uuid
|
id String @id @db.Uuid
|
||||||
name String @unique
|
name String @unique
|
||||||
@@ -44,6 +49,7 @@ model Court {
|
|||||||
priceRules CourtPriceRule[]
|
priceRules CourtPriceRule[]
|
||||||
bookings CourtBooking[]
|
bookings CourtBooking[]
|
||||||
maintenances CourtMaintenance[]
|
maintenances CourtMaintenance[]
|
||||||
|
recurringGroups RecurringBookingGroup[]
|
||||||
|
|
||||||
@@index([complexId])
|
@@index([complexId])
|
||||||
@@index([sportId])
|
@@index([sportId])
|
||||||
@@ -105,13 +111,16 @@ model CourtBooking {
|
|||||||
customerPhone String @map("customer_phone") @db.VarChar(30)
|
customerPhone String @map("customer_phone") @db.VarChar(30)
|
||||||
customerEmail String @map("customer_email") @db.VarChar(254)
|
customerEmail String @map("customer_email") @db.VarChar(254)
|
||||||
status CourtBookingStatus @default(CONFIRMED)
|
status CourtBookingStatus @default(CONFIRMED)
|
||||||
|
recurringGroupId String? @map("recurring_group_id") @db.Uuid
|
||||||
createdAt DateTime @default(now()) @map("created_at")
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
updatedAt DateTime @updatedAt @map("updated_at")
|
updatedAt DateTime @updatedAt @map("updated_at")
|
||||||
court Court @relation(fields: [courtId], references: [id], onDelete: Cascade)
|
court Court @relation(fields: [courtId], references: [id], onDelete: Cascade)
|
||||||
|
recurringGroup RecurringBookingGroup? @relation(fields: [recurringGroupId], references: [id])
|
||||||
|
|
||||||
@@unique([courtId, bookingDate, startTime])
|
@@unique([courtId, bookingDate, startTime])
|
||||||
@@index([courtId, bookingDate])
|
@@index([courtId, bookingDate])
|
||||||
@@index([bookingDate])
|
@@index([bookingDate])
|
||||||
|
@@index([recurringGroupId])
|
||||||
@@map("court_bookings")
|
@@map("court_bookings")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,6 +133,9 @@ model CourtBookingLog {
|
|||||||
endTime String @map("end_time") @db.VarChar(5)
|
endTime String @map("end_time") @db.VarChar(5)
|
||||||
previousStatus CourtBookingStatus @map("previous_status")
|
previousStatus CourtBookingStatus @map("previous_status")
|
||||||
newStatus CourtBookingStatus @map("new_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)
|
customerName String @map("customer_name") @db.VarChar(120)
|
||||||
customerPhone String @map("customer_phone") @db.VarChar(30)
|
customerPhone String @map("customer_phone") @db.VarChar(30)
|
||||||
customerEmail String @map("customer_email") @db.VarChar(254)
|
customerEmail String @map("customer_email") @db.VarChar(254)
|
||||||
@@ -132,3 +144,28 @@ model CourtBookingLog {
|
|||||||
@@map("court_booking_logs")
|
@@map("court_booking_logs")
|
||||||
@@index([courtId, newStatus])
|
@@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,
|
publicBookingPage: true,
|
||||||
advancedReports: false,
|
advancedReports: false,
|
||||||
whatsappReminders: false,
|
whatsappReminders: false,
|
||||||
|
fixedSlots: false,
|
||||||
},
|
},
|
||||||
pricing: {
|
pricing: {
|
||||||
overrides: {
|
overrides: {
|
||||||
@@ -51,6 +52,7 @@ export const planSeeds = [
|
|||||||
publicBookingPage: true,
|
publicBookingPage: true,
|
||||||
advancedReports: true,
|
advancedReports: true,
|
||||||
whatsappReminders: true,
|
whatsappReminders: true,
|
||||||
|
fixedSlots: true,
|
||||||
},
|
},
|
||||||
pricing: {
|
pricing: {
|
||||||
overrides: {
|
overrides: {
|
||||||
@@ -81,6 +83,7 @@ export const planSeeds = [
|
|||||||
publicBookingPage: true,
|
publicBookingPage: true,
|
||||||
advancedReports: true,
|
advancedReports: true,
|
||||||
whatsappReminders: true,
|
whatsappReminders: true,
|
||||||
|
fixedSlots: true,
|
||||||
},
|
},
|
||||||
pricing: {
|
pricing: {
|
||||||
overrides: {
|
overrides: {
|
||||||
|
|||||||
@@ -300,6 +300,172 @@ export function bookingCancelledHtml(data: BookingEmailData): string {
|
|||||||
return wrapLayout(content);
|
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 {
|
export function bookingNoShowHtml(data: BookingEmailData): string {
|
||||||
const friendlyDate = formatFriendlyDate(data.date);
|
const friendlyDate = formatFriendlyDate(data.date);
|
||||||
|
|
||||||
|
|||||||
@@ -87,6 +87,11 @@ export type CourtBooking = Prisma.CourtBookingModel
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export type CourtBookingLog = Prisma.CourtBookingLogModel
|
export type CourtBookingLog = Prisma.CourtBookingLogModel
|
||||||
|
/**
|
||||||
|
* Model RecurringBookingGroup
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type RecurringBookingGroup = Prisma.RecurringBookingGroupModel
|
||||||
/**
|
/**
|
||||||
* Model PasswordResetRequest
|
* Model PasswordResetRequest
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -111,6 +111,11 @@ export type CourtBooking = Prisma.CourtBookingModel
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export type CourtBookingLog = Prisma.CourtBookingLogModel
|
export type CourtBookingLog = Prisma.CourtBookingLogModel
|
||||||
|
/**
|
||||||
|
* Model RecurringBookingGroup
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type RecurringBookingGroup = Prisma.RecurringBookingGroupModel
|
||||||
/**
|
/**
|
||||||
* Model PasswordResetRequest
|
* Model PasswordResetRequest
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -314,6 +314,18 @@ export type EnumCourtBookingStatusFilter<$PrismaModel = never> = {
|
|||||||
not?: Prisma.NestedEnumCourtBookingStatusFilter<$PrismaModel> | $Enums.CourtBookingStatus
|
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> = {
|
export type EnumCourtBookingStatusWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
equals?: $Enums.CourtBookingStatus | Prisma.EnumCourtBookingStatusFieldRefInput<$PrismaModel>
|
equals?: $Enums.CourtBookingStatus | Prisma.EnumCourtBookingStatusFieldRefInput<$PrismaModel>
|
||||||
in?: $Enums.CourtBookingStatus[] | Prisma.ListEnumCourtBookingStatusFieldRefInput<$PrismaModel>
|
in?: $Enums.CourtBookingStatus[] | Prisma.ListEnumCourtBookingStatusFieldRefInput<$PrismaModel>
|
||||||
@@ -324,6 +336,38 @@ export type EnumCourtBookingStatusWithAggregatesFilter<$PrismaModel = never> = {
|
|||||||
_max?: Prisma.NestedEnumCourtBookingStatusFilter<$PrismaModel>
|
_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> =
|
export type JsonFilter<$PrismaModel = never> =
|
||||||
| Prisma.PatchUndefined<
|
| Prisma.PatchUndefined<
|
||||||
Prisma.Either<Required<JsonFilterBase<$PrismaModel>>, Exclude<keyof Required<JsonFilterBase<$PrismaModel>>, 'path'>>,
|
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
|
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> = {
|
export type NestedEnumCourtBookingStatusWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
equals?: $Enums.CourtBookingStatus | Prisma.EnumCourtBookingStatusFieldRefInput<$PrismaModel>
|
equals?: $Enums.CourtBookingStatus | Prisma.EnumCourtBookingStatusFieldRefInput<$PrismaModel>
|
||||||
in?: $Enums.CourtBookingStatus[] | Prisma.ListEnumCourtBookingStatusFieldRefInput<$PrismaModel>
|
in?: $Enums.CourtBookingStatus[] | Prisma.ListEnumCourtBookingStatusFieldRefInput<$PrismaModel>
|
||||||
@@ -696,6 +751,37 @@ export type NestedEnumCourtBookingStatusWithAggregatesFilter<$PrismaModel = neve
|
|||||||
_max?: Prisma.NestedEnumCourtBookingStatusFilter<$PrismaModel>
|
_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> =
|
export type NestedJsonFilter<$PrismaModel = never> =
|
||||||
| Prisma.PatchUndefined<
|
| Prisma.PatchUndefined<
|
||||||
Prisma.Either<Required<NestedJsonFilterBase<$PrismaModel>>, Exclude<keyof Required<NestedJsonFilterBase<$PrismaModel>>, 'path'>>,
|
Prisma.Either<Required<NestedJsonFilterBase<$PrismaModel>>, Exclude<keyof Required<NestedJsonFilterBase<$PrismaModel>>, 'path'>>,
|
||||||
|
|||||||
@@ -38,3 +38,11 @@ export const CourtBookingStatus = {
|
|||||||
} as const
|
} as const
|
||||||
|
|
||||||
export type CourtBookingStatus = (typeof CourtBookingStatus)[keyof typeof CourtBookingStatus]
|
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',
|
CourtPriceRule: 'CourtPriceRule',
|
||||||
CourtBooking: 'CourtBooking',
|
CourtBooking: 'CourtBooking',
|
||||||
CourtBookingLog: 'CourtBookingLog',
|
CourtBookingLog: 'CourtBookingLog',
|
||||||
|
RecurringBookingGroup: 'RecurringBookingGroup',
|
||||||
PasswordResetRequest: 'PasswordResetRequest',
|
PasswordResetRequest: 'PasswordResetRequest',
|
||||||
Plan: 'Plan'
|
Plan: 'Plan'
|
||||||
} as const
|
} as const
|
||||||
@@ -415,7 +416,7 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
|
|||||||
omit: GlobalOmitOptions
|
omit: GlobalOmitOptions
|
||||||
}
|
}
|
||||||
meta: {
|
meta: {
|
||||||
modelProps: "user" | "session" | "account" | "verification" | "complex" | "complexUser" | "complexInvitation" | "sport" | "court" | "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
|
txIsolationLevel: TransactionIsolationLevel
|
||||||
}
|
}
|
||||||
model: {
|
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: {
|
PasswordResetRequest: {
|
||||||
payload: Prisma.$PasswordResetRequestPayload<ExtArgs>
|
payload: Prisma.$PasswordResetRequestPayload<ExtArgs>
|
||||||
fields: Prisma.PasswordResetRequestFieldRefs
|
fields: Prisma.PasswordResetRequestFieldRefs
|
||||||
@@ -1832,6 +1907,7 @@ export const CourtBookingScalarFieldEnum = {
|
|||||||
customerPhone: 'customerPhone',
|
customerPhone: 'customerPhone',
|
||||||
customerEmail: 'customerEmail',
|
customerEmail: 'customerEmail',
|
||||||
status: 'status',
|
status: 'status',
|
||||||
|
recurringGroupId: 'recurringGroupId',
|
||||||
createdAt: 'createdAt',
|
createdAt: 'createdAt',
|
||||||
updatedAt: 'updatedAt'
|
updatedAt: 'updatedAt'
|
||||||
} as const
|
} as const
|
||||||
@@ -1848,6 +1924,9 @@ export const CourtBookingLogScalarFieldEnum = {
|
|||||||
endTime: 'endTime',
|
endTime: 'endTime',
|
||||||
previousStatus: 'previousStatus',
|
previousStatus: 'previousStatus',
|
||||||
newStatus: 'newStatus',
|
newStatus: 'newStatus',
|
||||||
|
previousCourtId: 'previousCourtId',
|
||||||
|
previousStartTime: 'previousStartTime',
|
||||||
|
previousEndTime: 'previousEndTime',
|
||||||
customerName: 'customerName',
|
customerName: 'customerName',
|
||||||
customerPhone: 'customerPhone',
|
customerPhone: 'customerPhone',
|
||||||
customerEmail: 'customerEmail',
|
customerEmail: 'customerEmail',
|
||||||
@@ -1857,6 +1936,26 @@ export const CourtBookingLogScalarFieldEnum = {
|
|||||||
export type CourtBookingLogScalarFieldEnum = (typeof CourtBookingLogScalarFieldEnum)[keyof typeof 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 = {
|
export const PasswordResetRequestScalarFieldEnum = {
|
||||||
id: 'id',
|
id: 'id',
|
||||||
email: 'email',
|
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'
|
* Reference to a field of type 'Json'
|
||||||
*/
|
*/
|
||||||
@@ -2171,6 +2284,7 @@ export type GlobalOmitConfig = {
|
|||||||
courtPriceRule?: Prisma.CourtPriceRuleOmit
|
courtPriceRule?: Prisma.CourtPriceRuleOmit
|
||||||
courtBooking?: Prisma.CourtBookingOmit
|
courtBooking?: Prisma.CourtBookingOmit
|
||||||
courtBookingLog?: Prisma.CourtBookingLogOmit
|
courtBookingLog?: Prisma.CourtBookingLogOmit
|
||||||
|
recurringBookingGroup?: Prisma.RecurringBookingGroupOmit
|
||||||
passwordResetRequest?: Prisma.PasswordResetRequestOmit
|
passwordResetRequest?: Prisma.PasswordResetRequestOmit
|
||||||
plan?: Prisma.PlanOmit
|
plan?: Prisma.PlanOmit
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ export const ModelName = {
|
|||||||
CourtPriceRule: 'CourtPriceRule',
|
CourtPriceRule: 'CourtPriceRule',
|
||||||
CourtBooking: 'CourtBooking',
|
CourtBooking: 'CourtBooking',
|
||||||
CourtBookingLog: 'CourtBookingLog',
|
CourtBookingLog: 'CourtBookingLog',
|
||||||
|
RecurringBookingGroup: 'RecurringBookingGroup',
|
||||||
PasswordResetRequest: 'PasswordResetRequest',
|
PasswordResetRequest: 'PasswordResetRequest',
|
||||||
Plan: 'Plan'
|
Plan: 'Plan'
|
||||||
} as const
|
} as const
|
||||||
@@ -275,6 +276,7 @@ export const CourtBookingScalarFieldEnum = {
|
|||||||
customerPhone: 'customerPhone',
|
customerPhone: 'customerPhone',
|
||||||
customerEmail: 'customerEmail',
|
customerEmail: 'customerEmail',
|
||||||
status: 'status',
|
status: 'status',
|
||||||
|
recurringGroupId: 'recurringGroupId',
|
||||||
createdAt: 'createdAt',
|
createdAt: 'createdAt',
|
||||||
updatedAt: 'updatedAt'
|
updatedAt: 'updatedAt'
|
||||||
} as const
|
} as const
|
||||||
@@ -291,6 +293,9 @@ export const CourtBookingLogScalarFieldEnum = {
|
|||||||
endTime: 'endTime',
|
endTime: 'endTime',
|
||||||
previousStatus: 'previousStatus',
|
previousStatus: 'previousStatus',
|
||||||
newStatus: 'newStatus',
|
newStatus: 'newStatus',
|
||||||
|
previousCourtId: 'previousCourtId',
|
||||||
|
previousStartTime: 'previousStartTime',
|
||||||
|
previousEndTime: 'previousEndTime',
|
||||||
customerName: 'customerName',
|
customerName: 'customerName',
|
||||||
customerPhone: 'customerPhone',
|
customerPhone: 'customerPhone',
|
||||||
customerEmail: 'customerEmail',
|
customerEmail: 'customerEmail',
|
||||||
@@ -300,6 +305,26 @@ export const CourtBookingLogScalarFieldEnum = {
|
|||||||
export type CourtBookingLogScalarFieldEnum = (typeof CourtBookingLogScalarFieldEnum)[keyof typeof 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 = {
|
export const PasswordResetRequestScalarFieldEnum = {
|
||||||
id: 'id',
|
id: 'id',
|
||||||
email: 'email',
|
email: 'email',
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export type * from './models/CourtAvailability'
|
|||||||
export type * from './models/CourtPriceRule'
|
export type * from './models/CourtPriceRule'
|
||||||
export type * from './models/CourtBooking'
|
export type * from './models/CourtBooking'
|
||||||
export type * from './models/CourtBookingLog'
|
export type * from './models/CourtBookingLog'
|
||||||
|
export type * from './models/RecurringBookingGroup'
|
||||||
export type * from './models/PasswordResetRequest'
|
export type * from './models/PasswordResetRequest'
|
||||||
export type * from './models/Plan'
|
export type * from './models/Plan'
|
||||||
export type * from './commonInputTypes'
|
export type * from './commonInputTypes'
|
||||||
@@ -234,6 +234,7 @@ export type ComplexWhereInput = {
|
|||||||
users?: Prisma.ComplexUserListRelationFilter
|
users?: Prisma.ComplexUserListRelationFilter
|
||||||
invitations?: Prisma.ComplexInvitationListRelationFilter
|
invitations?: Prisma.ComplexInvitationListRelationFilter
|
||||||
courts?: Prisma.CourtListRelationFilter
|
courts?: Prisma.CourtListRelationFilter
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupListRelationFilter
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ComplexOrderByWithRelationInput = {
|
export type ComplexOrderByWithRelationInput = {
|
||||||
@@ -252,6 +253,7 @@ export type ComplexOrderByWithRelationInput = {
|
|||||||
users?: Prisma.ComplexUserOrderByRelationAggregateInput
|
users?: Prisma.ComplexUserOrderByRelationAggregateInput
|
||||||
invitations?: Prisma.ComplexInvitationOrderByRelationAggregateInput
|
invitations?: Prisma.ComplexInvitationOrderByRelationAggregateInput
|
||||||
courts?: Prisma.CourtOrderByRelationAggregateInput
|
courts?: Prisma.CourtOrderByRelationAggregateInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupOrderByRelationAggregateInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ComplexWhereUniqueInput = Prisma.AtLeast<{
|
export type ComplexWhereUniqueInput = Prisma.AtLeast<{
|
||||||
@@ -273,6 +275,7 @@ export type ComplexWhereUniqueInput = Prisma.AtLeast<{
|
|||||||
users?: Prisma.ComplexUserListRelationFilter
|
users?: Prisma.ComplexUserListRelationFilter
|
||||||
invitations?: Prisma.ComplexInvitationListRelationFilter
|
invitations?: Prisma.ComplexInvitationListRelationFilter
|
||||||
courts?: Prisma.CourtListRelationFilter
|
courts?: Prisma.CourtListRelationFilter
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupListRelationFilter
|
||||||
}, "id" | "complexSlug">
|
}, "id" | "complexSlug">
|
||||||
|
|
||||||
export type ComplexOrderByWithAggregationInput = {
|
export type ComplexOrderByWithAggregationInput = {
|
||||||
@@ -324,6 +327,7 @@ export type ComplexCreateInput = {
|
|||||||
users?: Prisma.ComplexUserCreateNestedManyWithoutComplexInput
|
users?: Prisma.ComplexUserCreateNestedManyWithoutComplexInput
|
||||||
invitations?: Prisma.ComplexInvitationCreateNestedManyWithoutComplexInput
|
invitations?: Prisma.ComplexInvitationCreateNestedManyWithoutComplexInput
|
||||||
courts?: Prisma.CourtCreateNestedManyWithoutComplexInput
|
courts?: Prisma.CourtCreateNestedManyWithoutComplexInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupCreateNestedManyWithoutComplexInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ComplexUncheckedCreateInput = {
|
export type ComplexUncheckedCreateInput = {
|
||||||
@@ -341,6 +345,7 @@ export type ComplexUncheckedCreateInput = {
|
|||||||
users?: Prisma.ComplexUserUncheckedCreateNestedManyWithoutComplexInput
|
users?: Prisma.ComplexUserUncheckedCreateNestedManyWithoutComplexInput
|
||||||
invitations?: Prisma.ComplexInvitationUncheckedCreateNestedManyWithoutComplexInput
|
invitations?: Prisma.ComplexInvitationUncheckedCreateNestedManyWithoutComplexInput
|
||||||
courts?: Prisma.CourtUncheckedCreateNestedManyWithoutComplexInput
|
courts?: Prisma.CourtUncheckedCreateNestedManyWithoutComplexInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupUncheckedCreateNestedManyWithoutComplexInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ComplexUpdateInput = {
|
export type ComplexUpdateInput = {
|
||||||
@@ -358,6 +363,7 @@ export type ComplexUpdateInput = {
|
|||||||
users?: Prisma.ComplexUserUpdateManyWithoutComplexNestedInput
|
users?: Prisma.ComplexUserUpdateManyWithoutComplexNestedInput
|
||||||
invitations?: Prisma.ComplexInvitationUpdateManyWithoutComplexNestedInput
|
invitations?: Prisma.ComplexInvitationUpdateManyWithoutComplexNestedInput
|
||||||
courts?: Prisma.CourtUpdateManyWithoutComplexNestedInput
|
courts?: Prisma.CourtUpdateManyWithoutComplexNestedInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupUpdateManyWithoutComplexNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ComplexUncheckedUpdateInput = {
|
export type ComplexUncheckedUpdateInput = {
|
||||||
@@ -375,6 +381,7 @@ export type ComplexUncheckedUpdateInput = {
|
|||||||
users?: Prisma.ComplexUserUncheckedUpdateManyWithoutComplexNestedInput
|
users?: Prisma.ComplexUserUncheckedUpdateManyWithoutComplexNestedInput
|
||||||
invitations?: Prisma.ComplexInvitationUncheckedUpdateManyWithoutComplexNestedInput
|
invitations?: Prisma.ComplexInvitationUncheckedUpdateManyWithoutComplexNestedInput
|
||||||
courts?: Prisma.CourtUncheckedUpdateManyWithoutComplexNestedInput
|
courts?: Prisma.CourtUncheckedUpdateManyWithoutComplexNestedInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupUncheckedUpdateManyWithoutComplexNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ComplexCreateManyInput = {
|
export type ComplexCreateManyInput = {
|
||||||
@@ -517,6 +524,20 @@ export type ComplexUpdateOneRequiredWithoutCourtsNestedInput = {
|
|||||||
update?: Prisma.XOR<Prisma.XOR<Prisma.ComplexUpdateToOneWithWhereWithoutCourtsInput, Prisma.ComplexUpdateWithoutCourtsInput>, Prisma.ComplexUncheckedUpdateWithoutCourtsInput>
|
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 = {
|
export type ComplexCreateNestedManyWithoutPlanInput = {
|
||||||
create?: Prisma.XOR<Prisma.ComplexCreateWithoutPlanInput, Prisma.ComplexUncheckedCreateWithoutPlanInput> | Prisma.ComplexCreateWithoutPlanInput[] | Prisma.ComplexUncheckedCreateWithoutPlanInput[]
|
create?: Prisma.XOR<Prisma.ComplexCreateWithoutPlanInput, Prisma.ComplexUncheckedCreateWithoutPlanInput> | Prisma.ComplexCreateWithoutPlanInput[] | Prisma.ComplexUncheckedCreateWithoutPlanInput[]
|
||||||
connectOrCreate?: Prisma.ComplexCreateOrConnectWithoutPlanInput | Prisma.ComplexCreateOrConnectWithoutPlanInput[]
|
connectOrCreate?: Prisma.ComplexCreateOrConnectWithoutPlanInput | Prisma.ComplexCreateOrConnectWithoutPlanInput[]
|
||||||
@@ -573,6 +594,7 @@ export type ComplexCreateWithoutUsersInput = {
|
|||||||
plan?: Prisma.PlanCreateNestedOneWithoutComplexesInput
|
plan?: Prisma.PlanCreateNestedOneWithoutComplexesInput
|
||||||
invitations?: Prisma.ComplexInvitationCreateNestedManyWithoutComplexInput
|
invitations?: Prisma.ComplexInvitationCreateNestedManyWithoutComplexInput
|
||||||
courts?: Prisma.CourtCreateNestedManyWithoutComplexInput
|
courts?: Prisma.CourtCreateNestedManyWithoutComplexInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupCreateNestedManyWithoutComplexInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ComplexUncheckedCreateWithoutUsersInput = {
|
export type ComplexUncheckedCreateWithoutUsersInput = {
|
||||||
@@ -589,6 +611,7 @@ export type ComplexUncheckedCreateWithoutUsersInput = {
|
|||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
invitations?: Prisma.ComplexInvitationUncheckedCreateNestedManyWithoutComplexInput
|
invitations?: Prisma.ComplexInvitationUncheckedCreateNestedManyWithoutComplexInput
|
||||||
courts?: Prisma.CourtUncheckedCreateNestedManyWithoutComplexInput
|
courts?: Prisma.CourtUncheckedCreateNestedManyWithoutComplexInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupUncheckedCreateNestedManyWithoutComplexInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ComplexCreateOrConnectWithoutUsersInput = {
|
export type ComplexCreateOrConnectWithoutUsersInput = {
|
||||||
@@ -621,6 +644,7 @@ export type ComplexUpdateWithoutUsersInput = {
|
|||||||
plan?: Prisma.PlanUpdateOneWithoutComplexesNestedInput
|
plan?: Prisma.PlanUpdateOneWithoutComplexesNestedInput
|
||||||
invitations?: Prisma.ComplexInvitationUpdateManyWithoutComplexNestedInput
|
invitations?: Prisma.ComplexInvitationUpdateManyWithoutComplexNestedInput
|
||||||
courts?: Prisma.CourtUpdateManyWithoutComplexNestedInput
|
courts?: Prisma.CourtUpdateManyWithoutComplexNestedInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupUpdateManyWithoutComplexNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ComplexUncheckedUpdateWithoutUsersInput = {
|
export type ComplexUncheckedUpdateWithoutUsersInput = {
|
||||||
@@ -637,6 +661,7 @@ export type ComplexUncheckedUpdateWithoutUsersInput = {
|
|||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
invitations?: Prisma.ComplexInvitationUncheckedUpdateManyWithoutComplexNestedInput
|
invitations?: Prisma.ComplexInvitationUncheckedUpdateManyWithoutComplexNestedInput
|
||||||
courts?: Prisma.CourtUncheckedUpdateManyWithoutComplexNestedInput
|
courts?: Prisma.CourtUncheckedUpdateManyWithoutComplexNestedInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupUncheckedUpdateManyWithoutComplexNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ComplexCreateWithoutInvitationsInput = {
|
export type ComplexCreateWithoutInvitationsInput = {
|
||||||
@@ -653,6 +678,7 @@ export type ComplexCreateWithoutInvitationsInput = {
|
|||||||
plan?: Prisma.PlanCreateNestedOneWithoutComplexesInput
|
plan?: Prisma.PlanCreateNestedOneWithoutComplexesInput
|
||||||
users?: Prisma.ComplexUserCreateNestedManyWithoutComplexInput
|
users?: Prisma.ComplexUserCreateNestedManyWithoutComplexInput
|
||||||
courts?: Prisma.CourtCreateNestedManyWithoutComplexInput
|
courts?: Prisma.CourtCreateNestedManyWithoutComplexInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupCreateNestedManyWithoutComplexInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ComplexUncheckedCreateWithoutInvitationsInput = {
|
export type ComplexUncheckedCreateWithoutInvitationsInput = {
|
||||||
@@ -669,6 +695,7 @@ export type ComplexUncheckedCreateWithoutInvitationsInput = {
|
|||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
users?: Prisma.ComplexUserUncheckedCreateNestedManyWithoutComplexInput
|
users?: Prisma.ComplexUserUncheckedCreateNestedManyWithoutComplexInput
|
||||||
courts?: Prisma.CourtUncheckedCreateNestedManyWithoutComplexInput
|
courts?: Prisma.CourtUncheckedCreateNestedManyWithoutComplexInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupUncheckedCreateNestedManyWithoutComplexInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ComplexCreateOrConnectWithoutInvitationsInput = {
|
export type ComplexCreateOrConnectWithoutInvitationsInput = {
|
||||||
@@ -701,6 +728,7 @@ export type ComplexUpdateWithoutInvitationsInput = {
|
|||||||
plan?: Prisma.PlanUpdateOneWithoutComplexesNestedInput
|
plan?: Prisma.PlanUpdateOneWithoutComplexesNestedInput
|
||||||
users?: Prisma.ComplexUserUpdateManyWithoutComplexNestedInput
|
users?: Prisma.ComplexUserUpdateManyWithoutComplexNestedInput
|
||||||
courts?: Prisma.CourtUpdateManyWithoutComplexNestedInput
|
courts?: Prisma.CourtUpdateManyWithoutComplexNestedInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupUpdateManyWithoutComplexNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ComplexUncheckedUpdateWithoutInvitationsInput = {
|
export type ComplexUncheckedUpdateWithoutInvitationsInput = {
|
||||||
@@ -717,6 +745,7 @@ export type ComplexUncheckedUpdateWithoutInvitationsInput = {
|
|||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
users?: Prisma.ComplexUserUncheckedUpdateManyWithoutComplexNestedInput
|
users?: Prisma.ComplexUserUncheckedUpdateManyWithoutComplexNestedInput
|
||||||
courts?: Prisma.CourtUncheckedUpdateManyWithoutComplexNestedInput
|
courts?: Prisma.CourtUncheckedUpdateManyWithoutComplexNestedInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupUncheckedUpdateManyWithoutComplexNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ComplexCreateWithoutCourtsInput = {
|
export type ComplexCreateWithoutCourtsInput = {
|
||||||
@@ -733,6 +762,7 @@ export type ComplexCreateWithoutCourtsInput = {
|
|||||||
plan?: Prisma.PlanCreateNestedOneWithoutComplexesInput
|
plan?: Prisma.PlanCreateNestedOneWithoutComplexesInput
|
||||||
users?: Prisma.ComplexUserCreateNestedManyWithoutComplexInput
|
users?: Prisma.ComplexUserCreateNestedManyWithoutComplexInput
|
||||||
invitations?: Prisma.ComplexInvitationCreateNestedManyWithoutComplexInput
|
invitations?: Prisma.ComplexInvitationCreateNestedManyWithoutComplexInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupCreateNestedManyWithoutComplexInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ComplexUncheckedCreateWithoutCourtsInput = {
|
export type ComplexUncheckedCreateWithoutCourtsInput = {
|
||||||
@@ -749,6 +779,7 @@ export type ComplexUncheckedCreateWithoutCourtsInput = {
|
|||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
users?: Prisma.ComplexUserUncheckedCreateNestedManyWithoutComplexInput
|
users?: Prisma.ComplexUserUncheckedCreateNestedManyWithoutComplexInput
|
||||||
invitations?: Prisma.ComplexInvitationUncheckedCreateNestedManyWithoutComplexInput
|
invitations?: Prisma.ComplexInvitationUncheckedCreateNestedManyWithoutComplexInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupUncheckedCreateNestedManyWithoutComplexInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ComplexCreateOrConnectWithoutCourtsInput = {
|
export type ComplexCreateOrConnectWithoutCourtsInput = {
|
||||||
@@ -781,6 +812,7 @@ export type ComplexUpdateWithoutCourtsInput = {
|
|||||||
plan?: Prisma.PlanUpdateOneWithoutComplexesNestedInput
|
plan?: Prisma.PlanUpdateOneWithoutComplexesNestedInput
|
||||||
users?: Prisma.ComplexUserUpdateManyWithoutComplexNestedInput
|
users?: Prisma.ComplexUserUpdateManyWithoutComplexNestedInput
|
||||||
invitations?: Prisma.ComplexInvitationUpdateManyWithoutComplexNestedInput
|
invitations?: Prisma.ComplexInvitationUpdateManyWithoutComplexNestedInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupUpdateManyWithoutComplexNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ComplexUncheckedUpdateWithoutCourtsInput = {
|
export type ComplexUncheckedUpdateWithoutCourtsInput = {
|
||||||
@@ -797,6 +829,91 @@ export type ComplexUncheckedUpdateWithoutCourtsInput = {
|
|||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
users?: Prisma.ComplexUserUncheckedUpdateManyWithoutComplexNestedInput
|
users?: Prisma.ComplexUserUncheckedUpdateManyWithoutComplexNestedInput
|
||||||
invitations?: Prisma.ComplexInvitationUncheckedUpdateManyWithoutComplexNestedInput
|
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 = {
|
export type ComplexCreateWithoutPlanInput = {
|
||||||
@@ -813,6 +930,7 @@ export type ComplexCreateWithoutPlanInput = {
|
|||||||
users?: Prisma.ComplexUserCreateNestedManyWithoutComplexInput
|
users?: Prisma.ComplexUserCreateNestedManyWithoutComplexInput
|
||||||
invitations?: Prisma.ComplexInvitationCreateNestedManyWithoutComplexInput
|
invitations?: Prisma.ComplexInvitationCreateNestedManyWithoutComplexInput
|
||||||
courts?: Prisma.CourtCreateNestedManyWithoutComplexInput
|
courts?: Prisma.CourtCreateNestedManyWithoutComplexInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupCreateNestedManyWithoutComplexInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ComplexUncheckedCreateWithoutPlanInput = {
|
export type ComplexUncheckedCreateWithoutPlanInput = {
|
||||||
@@ -829,6 +947,7 @@ export type ComplexUncheckedCreateWithoutPlanInput = {
|
|||||||
users?: Prisma.ComplexUserUncheckedCreateNestedManyWithoutComplexInput
|
users?: Prisma.ComplexUserUncheckedCreateNestedManyWithoutComplexInput
|
||||||
invitations?: Prisma.ComplexInvitationUncheckedCreateNestedManyWithoutComplexInput
|
invitations?: Prisma.ComplexInvitationUncheckedCreateNestedManyWithoutComplexInput
|
||||||
courts?: Prisma.CourtUncheckedCreateNestedManyWithoutComplexInput
|
courts?: Prisma.CourtUncheckedCreateNestedManyWithoutComplexInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupUncheckedCreateNestedManyWithoutComplexInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ComplexCreateOrConnectWithoutPlanInput = {
|
export type ComplexCreateOrConnectWithoutPlanInput = {
|
||||||
@@ -901,6 +1020,7 @@ export type ComplexUpdateWithoutPlanInput = {
|
|||||||
users?: Prisma.ComplexUserUpdateManyWithoutComplexNestedInput
|
users?: Prisma.ComplexUserUpdateManyWithoutComplexNestedInput
|
||||||
invitations?: Prisma.ComplexInvitationUpdateManyWithoutComplexNestedInput
|
invitations?: Prisma.ComplexInvitationUpdateManyWithoutComplexNestedInput
|
||||||
courts?: Prisma.CourtUpdateManyWithoutComplexNestedInput
|
courts?: Prisma.CourtUpdateManyWithoutComplexNestedInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupUpdateManyWithoutComplexNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ComplexUncheckedUpdateWithoutPlanInput = {
|
export type ComplexUncheckedUpdateWithoutPlanInput = {
|
||||||
@@ -917,6 +1037,7 @@ export type ComplexUncheckedUpdateWithoutPlanInput = {
|
|||||||
users?: Prisma.ComplexUserUncheckedUpdateManyWithoutComplexNestedInput
|
users?: Prisma.ComplexUserUncheckedUpdateManyWithoutComplexNestedInput
|
||||||
invitations?: Prisma.ComplexInvitationUncheckedUpdateManyWithoutComplexNestedInput
|
invitations?: Prisma.ComplexInvitationUncheckedUpdateManyWithoutComplexNestedInput
|
||||||
courts?: Prisma.CourtUncheckedUpdateManyWithoutComplexNestedInput
|
courts?: Prisma.CourtUncheckedUpdateManyWithoutComplexNestedInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupUncheckedUpdateManyWithoutComplexNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ComplexUncheckedUpdateManyWithoutPlanInput = {
|
export type ComplexUncheckedUpdateManyWithoutPlanInput = {
|
||||||
@@ -941,12 +1062,14 @@ export type ComplexCountOutputType = {
|
|||||||
users: number
|
users: number
|
||||||
invitations: number
|
invitations: number
|
||||||
courts: number
|
courts: number
|
||||||
|
recurringGroups: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ComplexCountOutputTypeSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
export type ComplexCountOutputTypeSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
users?: boolean | ComplexCountOutputTypeCountUsersArgs
|
users?: boolean | ComplexCountOutputTypeCountUsersArgs
|
||||||
invitations?: boolean | ComplexCountOutputTypeCountInvitationsArgs
|
invitations?: boolean | ComplexCountOutputTypeCountInvitationsArgs
|
||||||
courts?: boolean | ComplexCountOutputTypeCountCourtsArgs
|
courts?: boolean | ComplexCountOutputTypeCountCourtsArgs
|
||||||
|
recurringGroups?: boolean | ComplexCountOutputTypeCountRecurringGroupsArgs
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -980,6 +1103,13 @@ export type ComplexCountOutputTypeCountCourtsArgs<ExtArgs extends runtime.Types.
|
|||||||
where?: Prisma.CourtWhereInput
|
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<{
|
export type ComplexSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
|
||||||
id?: boolean
|
id?: boolean
|
||||||
@@ -997,6 +1127,7 @@ export type ComplexSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs
|
|||||||
users?: boolean | Prisma.Complex$usersArgs<ExtArgs>
|
users?: boolean | Prisma.Complex$usersArgs<ExtArgs>
|
||||||
invitations?: boolean | Prisma.Complex$invitationsArgs<ExtArgs>
|
invitations?: boolean | Prisma.Complex$invitationsArgs<ExtArgs>
|
||||||
courts?: boolean | Prisma.Complex$courtsArgs<ExtArgs>
|
courts?: boolean | Prisma.Complex$courtsArgs<ExtArgs>
|
||||||
|
recurringGroups?: boolean | Prisma.Complex$recurringGroupsArgs<ExtArgs>
|
||||||
_count?: boolean | Prisma.ComplexCountOutputTypeDefaultArgs<ExtArgs>
|
_count?: boolean | Prisma.ComplexCountOutputTypeDefaultArgs<ExtArgs>
|
||||||
}, ExtArgs["result"]["complex"]>
|
}, ExtArgs["result"]["complex"]>
|
||||||
|
|
||||||
@@ -1050,6 +1181,7 @@ export type ComplexInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs
|
|||||||
users?: boolean | Prisma.Complex$usersArgs<ExtArgs>
|
users?: boolean | Prisma.Complex$usersArgs<ExtArgs>
|
||||||
invitations?: boolean | Prisma.Complex$invitationsArgs<ExtArgs>
|
invitations?: boolean | Prisma.Complex$invitationsArgs<ExtArgs>
|
||||||
courts?: boolean | Prisma.Complex$courtsArgs<ExtArgs>
|
courts?: boolean | Prisma.Complex$courtsArgs<ExtArgs>
|
||||||
|
recurringGroups?: boolean | Prisma.Complex$recurringGroupsArgs<ExtArgs>
|
||||||
_count?: boolean | Prisma.ComplexCountOutputTypeDefaultArgs<ExtArgs>
|
_count?: boolean | Prisma.ComplexCountOutputTypeDefaultArgs<ExtArgs>
|
||||||
}
|
}
|
||||||
export type ComplexIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
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>[]
|
users: Prisma.$ComplexUserPayload<ExtArgs>[]
|
||||||
invitations: Prisma.$ComplexInvitationPayload<ExtArgs>[]
|
invitations: Prisma.$ComplexInvitationPayload<ExtArgs>[]
|
||||||
courts: Prisma.$CourtPayload<ExtArgs>[]
|
courts: Prisma.$CourtPayload<ExtArgs>[]
|
||||||
|
recurringGroups: Prisma.$RecurringBookingGroupPayload<ExtArgs>[]
|
||||||
}
|
}
|
||||||
scalars: runtime.Types.Extensions.GetPayloadResult<{
|
scalars: runtime.Types.Extensions.GetPayloadResult<{
|
||||||
id: string
|
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>
|
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>
|
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>
|
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.
|
* Attaches callbacks for the resolution and/or rejection of the Promise.
|
||||||
* @param onfulfilled The callback to execute when the Promise is resolved.
|
* @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[]
|
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
|
* Complex without action
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -266,6 +266,7 @@ export type CourtWhereInput = {
|
|||||||
priceRules?: Prisma.CourtPriceRuleListRelationFilter
|
priceRules?: Prisma.CourtPriceRuleListRelationFilter
|
||||||
bookings?: Prisma.CourtBookingListRelationFilter
|
bookings?: Prisma.CourtBookingListRelationFilter
|
||||||
maintenances?: Prisma.CourtMaintenanceListRelationFilter
|
maintenances?: Prisma.CourtMaintenanceListRelationFilter
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupListRelationFilter
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CourtOrderByWithRelationInput = {
|
export type CourtOrderByWithRelationInput = {
|
||||||
@@ -285,6 +286,7 @@ export type CourtOrderByWithRelationInput = {
|
|||||||
priceRules?: Prisma.CourtPriceRuleOrderByRelationAggregateInput
|
priceRules?: Prisma.CourtPriceRuleOrderByRelationAggregateInput
|
||||||
bookings?: Prisma.CourtBookingOrderByRelationAggregateInput
|
bookings?: Prisma.CourtBookingOrderByRelationAggregateInput
|
||||||
maintenances?: Prisma.CourtMaintenanceOrderByRelationAggregateInput
|
maintenances?: Prisma.CourtMaintenanceOrderByRelationAggregateInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupOrderByRelationAggregateInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CourtWhereUniqueInput = Prisma.AtLeast<{
|
export type CourtWhereUniqueInput = Prisma.AtLeast<{
|
||||||
@@ -307,6 +309,7 @@ export type CourtWhereUniqueInput = Prisma.AtLeast<{
|
|||||||
priceRules?: Prisma.CourtPriceRuleListRelationFilter
|
priceRules?: Prisma.CourtPriceRuleListRelationFilter
|
||||||
bookings?: Prisma.CourtBookingListRelationFilter
|
bookings?: Prisma.CourtBookingListRelationFilter
|
||||||
maintenances?: Prisma.CourtMaintenanceListRelationFilter
|
maintenances?: Prisma.CourtMaintenanceListRelationFilter
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupListRelationFilter
|
||||||
}, "id">
|
}, "id">
|
||||||
|
|
||||||
export type CourtOrderByWithAggregationInput = {
|
export type CourtOrderByWithAggregationInput = {
|
||||||
@@ -358,6 +361,7 @@ export type CourtCreateInput = {
|
|||||||
priceRules?: Prisma.CourtPriceRuleCreateNestedManyWithoutCourtInput
|
priceRules?: Prisma.CourtPriceRuleCreateNestedManyWithoutCourtInput
|
||||||
bookings?: Prisma.CourtBookingCreateNestedManyWithoutCourtInput
|
bookings?: Prisma.CourtBookingCreateNestedManyWithoutCourtInput
|
||||||
maintenances?: Prisma.CourtMaintenanceCreateNestedManyWithoutCourtInput
|
maintenances?: Prisma.CourtMaintenanceCreateNestedManyWithoutCourtInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupCreateNestedManyWithoutCourtInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CourtUncheckedCreateInput = {
|
export type CourtUncheckedCreateInput = {
|
||||||
@@ -375,6 +379,7 @@ export type CourtUncheckedCreateInput = {
|
|||||||
priceRules?: Prisma.CourtPriceRuleUncheckedCreateNestedManyWithoutCourtInput
|
priceRules?: Prisma.CourtPriceRuleUncheckedCreateNestedManyWithoutCourtInput
|
||||||
bookings?: Prisma.CourtBookingUncheckedCreateNestedManyWithoutCourtInput
|
bookings?: Prisma.CourtBookingUncheckedCreateNestedManyWithoutCourtInput
|
||||||
maintenances?: Prisma.CourtMaintenanceUncheckedCreateNestedManyWithoutCourtInput
|
maintenances?: Prisma.CourtMaintenanceUncheckedCreateNestedManyWithoutCourtInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupUncheckedCreateNestedManyWithoutCourtInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CourtUpdateInput = {
|
export type CourtUpdateInput = {
|
||||||
@@ -392,6 +397,7 @@ export type CourtUpdateInput = {
|
|||||||
priceRules?: Prisma.CourtPriceRuleUpdateManyWithoutCourtNestedInput
|
priceRules?: Prisma.CourtPriceRuleUpdateManyWithoutCourtNestedInput
|
||||||
bookings?: Prisma.CourtBookingUpdateManyWithoutCourtNestedInput
|
bookings?: Prisma.CourtBookingUpdateManyWithoutCourtNestedInput
|
||||||
maintenances?: Prisma.CourtMaintenanceUpdateManyWithoutCourtNestedInput
|
maintenances?: Prisma.CourtMaintenanceUpdateManyWithoutCourtNestedInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupUpdateManyWithoutCourtNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CourtUncheckedUpdateInput = {
|
export type CourtUncheckedUpdateInput = {
|
||||||
@@ -409,6 +415,7 @@ export type CourtUncheckedUpdateInput = {
|
|||||||
priceRules?: Prisma.CourtPriceRuleUncheckedUpdateManyWithoutCourtNestedInput
|
priceRules?: Prisma.CourtPriceRuleUncheckedUpdateManyWithoutCourtNestedInput
|
||||||
bookings?: Prisma.CourtBookingUncheckedUpdateManyWithoutCourtNestedInput
|
bookings?: Prisma.CourtBookingUncheckedUpdateManyWithoutCourtNestedInput
|
||||||
maintenances?: Prisma.CourtMaintenanceUncheckedUpdateManyWithoutCourtNestedInput
|
maintenances?: Prisma.CourtMaintenanceUncheckedUpdateManyWithoutCourtNestedInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupUncheckedUpdateManyWithoutCourtNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CourtCreateManyInput = {
|
export type CourtCreateManyInput = {
|
||||||
@@ -668,6 +675,20 @@ export type CourtUpdateOneRequiredWithoutBookingsNestedInput = {
|
|||||||
update?: Prisma.XOR<Prisma.XOR<Prisma.CourtUpdateToOneWithWhereWithoutBookingsInput, Prisma.CourtUpdateWithoutBookingsInput>, Prisma.CourtUncheckedUpdateWithoutBookingsInput>
|
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 = {
|
export type CourtCreateWithoutComplexInput = {
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
@@ -682,6 +703,7 @@ export type CourtCreateWithoutComplexInput = {
|
|||||||
priceRules?: Prisma.CourtPriceRuleCreateNestedManyWithoutCourtInput
|
priceRules?: Prisma.CourtPriceRuleCreateNestedManyWithoutCourtInput
|
||||||
bookings?: Prisma.CourtBookingCreateNestedManyWithoutCourtInput
|
bookings?: Prisma.CourtBookingCreateNestedManyWithoutCourtInput
|
||||||
maintenances?: Prisma.CourtMaintenanceCreateNestedManyWithoutCourtInput
|
maintenances?: Prisma.CourtMaintenanceCreateNestedManyWithoutCourtInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupCreateNestedManyWithoutCourtInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CourtUncheckedCreateWithoutComplexInput = {
|
export type CourtUncheckedCreateWithoutComplexInput = {
|
||||||
@@ -698,6 +720,7 @@ export type CourtUncheckedCreateWithoutComplexInput = {
|
|||||||
priceRules?: Prisma.CourtPriceRuleUncheckedCreateNestedManyWithoutCourtInput
|
priceRules?: Prisma.CourtPriceRuleUncheckedCreateNestedManyWithoutCourtInput
|
||||||
bookings?: Prisma.CourtBookingUncheckedCreateNestedManyWithoutCourtInput
|
bookings?: Prisma.CourtBookingUncheckedCreateNestedManyWithoutCourtInput
|
||||||
maintenances?: Prisma.CourtMaintenanceUncheckedCreateNestedManyWithoutCourtInput
|
maintenances?: Prisma.CourtMaintenanceUncheckedCreateNestedManyWithoutCourtInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupUncheckedCreateNestedManyWithoutCourtInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CourtCreateOrConnectWithoutComplexInput = {
|
export type CourtCreateOrConnectWithoutComplexInput = {
|
||||||
@@ -756,6 +779,7 @@ export type CourtCreateWithoutSportInput = {
|
|||||||
priceRules?: Prisma.CourtPriceRuleCreateNestedManyWithoutCourtInput
|
priceRules?: Prisma.CourtPriceRuleCreateNestedManyWithoutCourtInput
|
||||||
bookings?: Prisma.CourtBookingCreateNestedManyWithoutCourtInput
|
bookings?: Prisma.CourtBookingCreateNestedManyWithoutCourtInput
|
||||||
maintenances?: Prisma.CourtMaintenanceCreateNestedManyWithoutCourtInput
|
maintenances?: Prisma.CourtMaintenanceCreateNestedManyWithoutCourtInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupCreateNestedManyWithoutCourtInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CourtUncheckedCreateWithoutSportInput = {
|
export type CourtUncheckedCreateWithoutSportInput = {
|
||||||
@@ -772,6 +796,7 @@ export type CourtUncheckedCreateWithoutSportInput = {
|
|||||||
priceRules?: Prisma.CourtPriceRuleUncheckedCreateNestedManyWithoutCourtInput
|
priceRules?: Prisma.CourtPriceRuleUncheckedCreateNestedManyWithoutCourtInput
|
||||||
bookings?: Prisma.CourtBookingUncheckedCreateNestedManyWithoutCourtInput
|
bookings?: Prisma.CourtBookingUncheckedCreateNestedManyWithoutCourtInput
|
||||||
maintenances?: Prisma.CourtMaintenanceUncheckedCreateNestedManyWithoutCourtInput
|
maintenances?: Prisma.CourtMaintenanceUncheckedCreateNestedManyWithoutCourtInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupUncheckedCreateNestedManyWithoutCourtInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CourtCreateOrConnectWithoutSportInput = {
|
export type CourtCreateOrConnectWithoutSportInput = {
|
||||||
@@ -814,6 +839,7 @@ export type CourtCreateWithoutMaintenancesInput = {
|
|||||||
availabilities?: Prisma.CourtAvailabilityCreateNestedManyWithoutCourtInput
|
availabilities?: Prisma.CourtAvailabilityCreateNestedManyWithoutCourtInput
|
||||||
priceRules?: Prisma.CourtPriceRuleCreateNestedManyWithoutCourtInput
|
priceRules?: Prisma.CourtPriceRuleCreateNestedManyWithoutCourtInput
|
||||||
bookings?: Prisma.CourtBookingCreateNestedManyWithoutCourtInput
|
bookings?: Prisma.CourtBookingCreateNestedManyWithoutCourtInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupCreateNestedManyWithoutCourtInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CourtUncheckedCreateWithoutMaintenancesInput = {
|
export type CourtUncheckedCreateWithoutMaintenancesInput = {
|
||||||
@@ -830,6 +856,7 @@ export type CourtUncheckedCreateWithoutMaintenancesInput = {
|
|||||||
availabilities?: Prisma.CourtAvailabilityUncheckedCreateNestedManyWithoutCourtInput
|
availabilities?: Prisma.CourtAvailabilityUncheckedCreateNestedManyWithoutCourtInput
|
||||||
priceRules?: Prisma.CourtPriceRuleUncheckedCreateNestedManyWithoutCourtInput
|
priceRules?: Prisma.CourtPriceRuleUncheckedCreateNestedManyWithoutCourtInput
|
||||||
bookings?: Prisma.CourtBookingUncheckedCreateNestedManyWithoutCourtInput
|
bookings?: Prisma.CourtBookingUncheckedCreateNestedManyWithoutCourtInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupUncheckedCreateNestedManyWithoutCourtInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CourtCreateOrConnectWithoutMaintenancesInput = {
|
export type CourtCreateOrConnectWithoutMaintenancesInput = {
|
||||||
@@ -862,6 +889,7 @@ export type CourtUpdateWithoutMaintenancesInput = {
|
|||||||
availabilities?: Prisma.CourtAvailabilityUpdateManyWithoutCourtNestedInput
|
availabilities?: Prisma.CourtAvailabilityUpdateManyWithoutCourtNestedInput
|
||||||
priceRules?: Prisma.CourtPriceRuleUpdateManyWithoutCourtNestedInput
|
priceRules?: Prisma.CourtPriceRuleUpdateManyWithoutCourtNestedInput
|
||||||
bookings?: Prisma.CourtBookingUpdateManyWithoutCourtNestedInput
|
bookings?: Prisma.CourtBookingUpdateManyWithoutCourtNestedInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupUpdateManyWithoutCourtNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CourtUncheckedUpdateWithoutMaintenancesInput = {
|
export type CourtUncheckedUpdateWithoutMaintenancesInput = {
|
||||||
@@ -878,6 +906,7 @@ export type CourtUncheckedUpdateWithoutMaintenancesInput = {
|
|||||||
availabilities?: Prisma.CourtAvailabilityUncheckedUpdateManyWithoutCourtNestedInput
|
availabilities?: Prisma.CourtAvailabilityUncheckedUpdateManyWithoutCourtNestedInput
|
||||||
priceRules?: Prisma.CourtPriceRuleUncheckedUpdateManyWithoutCourtNestedInput
|
priceRules?: Prisma.CourtPriceRuleUncheckedUpdateManyWithoutCourtNestedInput
|
||||||
bookings?: Prisma.CourtBookingUncheckedUpdateManyWithoutCourtNestedInput
|
bookings?: Prisma.CourtBookingUncheckedUpdateManyWithoutCourtNestedInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupUncheckedUpdateManyWithoutCourtNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CourtCreateWithoutAvailabilitiesInput = {
|
export type CourtCreateWithoutAvailabilitiesInput = {
|
||||||
@@ -894,6 +923,7 @@ export type CourtCreateWithoutAvailabilitiesInput = {
|
|||||||
priceRules?: Prisma.CourtPriceRuleCreateNestedManyWithoutCourtInput
|
priceRules?: Prisma.CourtPriceRuleCreateNestedManyWithoutCourtInput
|
||||||
bookings?: Prisma.CourtBookingCreateNestedManyWithoutCourtInput
|
bookings?: Prisma.CourtBookingCreateNestedManyWithoutCourtInput
|
||||||
maintenances?: Prisma.CourtMaintenanceCreateNestedManyWithoutCourtInput
|
maintenances?: Prisma.CourtMaintenanceCreateNestedManyWithoutCourtInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupCreateNestedManyWithoutCourtInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CourtUncheckedCreateWithoutAvailabilitiesInput = {
|
export type CourtUncheckedCreateWithoutAvailabilitiesInput = {
|
||||||
@@ -910,6 +940,7 @@ export type CourtUncheckedCreateWithoutAvailabilitiesInput = {
|
|||||||
priceRules?: Prisma.CourtPriceRuleUncheckedCreateNestedManyWithoutCourtInput
|
priceRules?: Prisma.CourtPriceRuleUncheckedCreateNestedManyWithoutCourtInput
|
||||||
bookings?: Prisma.CourtBookingUncheckedCreateNestedManyWithoutCourtInput
|
bookings?: Prisma.CourtBookingUncheckedCreateNestedManyWithoutCourtInput
|
||||||
maintenances?: Prisma.CourtMaintenanceUncheckedCreateNestedManyWithoutCourtInput
|
maintenances?: Prisma.CourtMaintenanceUncheckedCreateNestedManyWithoutCourtInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupUncheckedCreateNestedManyWithoutCourtInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CourtCreateOrConnectWithoutAvailabilitiesInput = {
|
export type CourtCreateOrConnectWithoutAvailabilitiesInput = {
|
||||||
@@ -942,6 +973,7 @@ export type CourtUpdateWithoutAvailabilitiesInput = {
|
|||||||
priceRules?: Prisma.CourtPriceRuleUpdateManyWithoutCourtNestedInput
|
priceRules?: Prisma.CourtPriceRuleUpdateManyWithoutCourtNestedInput
|
||||||
bookings?: Prisma.CourtBookingUpdateManyWithoutCourtNestedInput
|
bookings?: Prisma.CourtBookingUpdateManyWithoutCourtNestedInput
|
||||||
maintenances?: Prisma.CourtMaintenanceUpdateManyWithoutCourtNestedInput
|
maintenances?: Prisma.CourtMaintenanceUpdateManyWithoutCourtNestedInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupUpdateManyWithoutCourtNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CourtUncheckedUpdateWithoutAvailabilitiesInput = {
|
export type CourtUncheckedUpdateWithoutAvailabilitiesInput = {
|
||||||
@@ -958,6 +990,7 @@ export type CourtUncheckedUpdateWithoutAvailabilitiesInput = {
|
|||||||
priceRules?: Prisma.CourtPriceRuleUncheckedUpdateManyWithoutCourtNestedInput
|
priceRules?: Prisma.CourtPriceRuleUncheckedUpdateManyWithoutCourtNestedInput
|
||||||
bookings?: Prisma.CourtBookingUncheckedUpdateManyWithoutCourtNestedInput
|
bookings?: Prisma.CourtBookingUncheckedUpdateManyWithoutCourtNestedInput
|
||||||
maintenances?: Prisma.CourtMaintenanceUncheckedUpdateManyWithoutCourtNestedInput
|
maintenances?: Prisma.CourtMaintenanceUncheckedUpdateManyWithoutCourtNestedInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupUncheckedUpdateManyWithoutCourtNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CourtCreateWithoutPriceRulesInput = {
|
export type CourtCreateWithoutPriceRulesInput = {
|
||||||
@@ -974,6 +1007,7 @@ export type CourtCreateWithoutPriceRulesInput = {
|
|||||||
availabilities?: Prisma.CourtAvailabilityCreateNestedManyWithoutCourtInput
|
availabilities?: Prisma.CourtAvailabilityCreateNestedManyWithoutCourtInput
|
||||||
bookings?: Prisma.CourtBookingCreateNestedManyWithoutCourtInput
|
bookings?: Prisma.CourtBookingCreateNestedManyWithoutCourtInput
|
||||||
maintenances?: Prisma.CourtMaintenanceCreateNestedManyWithoutCourtInput
|
maintenances?: Prisma.CourtMaintenanceCreateNestedManyWithoutCourtInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupCreateNestedManyWithoutCourtInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CourtUncheckedCreateWithoutPriceRulesInput = {
|
export type CourtUncheckedCreateWithoutPriceRulesInput = {
|
||||||
@@ -990,6 +1024,7 @@ export type CourtUncheckedCreateWithoutPriceRulesInput = {
|
|||||||
availabilities?: Prisma.CourtAvailabilityUncheckedCreateNestedManyWithoutCourtInput
|
availabilities?: Prisma.CourtAvailabilityUncheckedCreateNestedManyWithoutCourtInput
|
||||||
bookings?: Prisma.CourtBookingUncheckedCreateNestedManyWithoutCourtInput
|
bookings?: Prisma.CourtBookingUncheckedCreateNestedManyWithoutCourtInput
|
||||||
maintenances?: Prisma.CourtMaintenanceUncheckedCreateNestedManyWithoutCourtInput
|
maintenances?: Prisma.CourtMaintenanceUncheckedCreateNestedManyWithoutCourtInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupUncheckedCreateNestedManyWithoutCourtInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CourtCreateOrConnectWithoutPriceRulesInput = {
|
export type CourtCreateOrConnectWithoutPriceRulesInput = {
|
||||||
@@ -1022,6 +1057,7 @@ export type CourtUpdateWithoutPriceRulesInput = {
|
|||||||
availabilities?: Prisma.CourtAvailabilityUpdateManyWithoutCourtNestedInput
|
availabilities?: Prisma.CourtAvailabilityUpdateManyWithoutCourtNestedInput
|
||||||
bookings?: Prisma.CourtBookingUpdateManyWithoutCourtNestedInput
|
bookings?: Prisma.CourtBookingUpdateManyWithoutCourtNestedInput
|
||||||
maintenances?: Prisma.CourtMaintenanceUpdateManyWithoutCourtNestedInput
|
maintenances?: Prisma.CourtMaintenanceUpdateManyWithoutCourtNestedInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupUpdateManyWithoutCourtNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CourtUncheckedUpdateWithoutPriceRulesInput = {
|
export type CourtUncheckedUpdateWithoutPriceRulesInput = {
|
||||||
@@ -1038,6 +1074,7 @@ export type CourtUncheckedUpdateWithoutPriceRulesInput = {
|
|||||||
availabilities?: Prisma.CourtAvailabilityUncheckedUpdateManyWithoutCourtNestedInput
|
availabilities?: Prisma.CourtAvailabilityUncheckedUpdateManyWithoutCourtNestedInput
|
||||||
bookings?: Prisma.CourtBookingUncheckedUpdateManyWithoutCourtNestedInput
|
bookings?: Prisma.CourtBookingUncheckedUpdateManyWithoutCourtNestedInput
|
||||||
maintenances?: Prisma.CourtMaintenanceUncheckedUpdateManyWithoutCourtNestedInput
|
maintenances?: Prisma.CourtMaintenanceUncheckedUpdateManyWithoutCourtNestedInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupUncheckedUpdateManyWithoutCourtNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CourtCreateWithoutBookingsInput = {
|
export type CourtCreateWithoutBookingsInput = {
|
||||||
@@ -1054,6 +1091,7 @@ export type CourtCreateWithoutBookingsInput = {
|
|||||||
availabilities?: Prisma.CourtAvailabilityCreateNestedManyWithoutCourtInput
|
availabilities?: Prisma.CourtAvailabilityCreateNestedManyWithoutCourtInput
|
||||||
priceRules?: Prisma.CourtPriceRuleCreateNestedManyWithoutCourtInput
|
priceRules?: Prisma.CourtPriceRuleCreateNestedManyWithoutCourtInput
|
||||||
maintenances?: Prisma.CourtMaintenanceCreateNestedManyWithoutCourtInput
|
maintenances?: Prisma.CourtMaintenanceCreateNestedManyWithoutCourtInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupCreateNestedManyWithoutCourtInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CourtUncheckedCreateWithoutBookingsInput = {
|
export type CourtUncheckedCreateWithoutBookingsInput = {
|
||||||
@@ -1070,6 +1108,7 @@ export type CourtUncheckedCreateWithoutBookingsInput = {
|
|||||||
availabilities?: Prisma.CourtAvailabilityUncheckedCreateNestedManyWithoutCourtInput
|
availabilities?: Prisma.CourtAvailabilityUncheckedCreateNestedManyWithoutCourtInput
|
||||||
priceRules?: Prisma.CourtPriceRuleUncheckedCreateNestedManyWithoutCourtInput
|
priceRules?: Prisma.CourtPriceRuleUncheckedCreateNestedManyWithoutCourtInput
|
||||||
maintenances?: Prisma.CourtMaintenanceUncheckedCreateNestedManyWithoutCourtInput
|
maintenances?: Prisma.CourtMaintenanceUncheckedCreateNestedManyWithoutCourtInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupUncheckedCreateNestedManyWithoutCourtInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CourtCreateOrConnectWithoutBookingsInput = {
|
export type CourtCreateOrConnectWithoutBookingsInput = {
|
||||||
@@ -1102,6 +1141,7 @@ export type CourtUpdateWithoutBookingsInput = {
|
|||||||
availabilities?: Prisma.CourtAvailabilityUpdateManyWithoutCourtNestedInput
|
availabilities?: Prisma.CourtAvailabilityUpdateManyWithoutCourtNestedInput
|
||||||
priceRules?: Prisma.CourtPriceRuleUpdateManyWithoutCourtNestedInput
|
priceRules?: Prisma.CourtPriceRuleUpdateManyWithoutCourtNestedInput
|
||||||
maintenances?: Prisma.CourtMaintenanceUpdateManyWithoutCourtNestedInput
|
maintenances?: Prisma.CourtMaintenanceUpdateManyWithoutCourtNestedInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupUpdateManyWithoutCourtNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CourtUncheckedUpdateWithoutBookingsInput = {
|
export type CourtUncheckedUpdateWithoutBookingsInput = {
|
||||||
@@ -1118,6 +1158,91 @@ export type CourtUncheckedUpdateWithoutBookingsInput = {
|
|||||||
availabilities?: Prisma.CourtAvailabilityUncheckedUpdateManyWithoutCourtNestedInput
|
availabilities?: Prisma.CourtAvailabilityUncheckedUpdateManyWithoutCourtNestedInput
|
||||||
priceRules?: Prisma.CourtPriceRuleUncheckedUpdateManyWithoutCourtNestedInput
|
priceRules?: Prisma.CourtPriceRuleUncheckedUpdateManyWithoutCourtNestedInput
|
||||||
maintenances?: Prisma.CourtMaintenanceUncheckedUpdateManyWithoutCourtNestedInput
|
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 = {
|
export type CourtCreateManyComplexInput = {
|
||||||
@@ -1146,6 +1271,7 @@ export type CourtUpdateWithoutComplexInput = {
|
|||||||
priceRules?: Prisma.CourtPriceRuleUpdateManyWithoutCourtNestedInput
|
priceRules?: Prisma.CourtPriceRuleUpdateManyWithoutCourtNestedInput
|
||||||
bookings?: Prisma.CourtBookingUpdateManyWithoutCourtNestedInput
|
bookings?: Prisma.CourtBookingUpdateManyWithoutCourtNestedInput
|
||||||
maintenances?: Prisma.CourtMaintenanceUpdateManyWithoutCourtNestedInput
|
maintenances?: Prisma.CourtMaintenanceUpdateManyWithoutCourtNestedInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupUpdateManyWithoutCourtNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CourtUncheckedUpdateWithoutComplexInput = {
|
export type CourtUncheckedUpdateWithoutComplexInput = {
|
||||||
@@ -1162,6 +1288,7 @@ export type CourtUncheckedUpdateWithoutComplexInput = {
|
|||||||
priceRules?: Prisma.CourtPriceRuleUncheckedUpdateManyWithoutCourtNestedInput
|
priceRules?: Prisma.CourtPriceRuleUncheckedUpdateManyWithoutCourtNestedInput
|
||||||
bookings?: Prisma.CourtBookingUncheckedUpdateManyWithoutCourtNestedInput
|
bookings?: Prisma.CourtBookingUncheckedUpdateManyWithoutCourtNestedInput
|
||||||
maintenances?: Prisma.CourtMaintenanceUncheckedUpdateManyWithoutCourtNestedInput
|
maintenances?: Prisma.CourtMaintenanceUncheckedUpdateManyWithoutCourtNestedInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupUncheckedUpdateManyWithoutCourtNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CourtUncheckedUpdateManyWithoutComplexInput = {
|
export type CourtUncheckedUpdateManyWithoutComplexInput = {
|
||||||
@@ -1202,6 +1329,7 @@ export type CourtUpdateWithoutSportInput = {
|
|||||||
priceRules?: Prisma.CourtPriceRuleUpdateManyWithoutCourtNestedInput
|
priceRules?: Prisma.CourtPriceRuleUpdateManyWithoutCourtNestedInput
|
||||||
bookings?: Prisma.CourtBookingUpdateManyWithoutCourtNestedInput
|
bookings?: Prisma.CourtBookingUpdateManyWithoutCourtNestedInput
|
||||||
maintenances?: Prisma.CourtMaintenanceUpdateManyWithoutCourtNestedInput
|
maintenances?: Prisma.CourtMaintenanceUpdateManyWithoutCourtNestedInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupUpdateManyWithoutCourtNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CourtUncheckedUpdateWithoutSportInput = {
|
export type CourtUncheckedUpdateWithoutSportInput = {
|
||||||
@@ -1218,6 +1346,7 @@ export type CourtUncheckedUpdateWithoutSportInput = {
|
|||||||
priceRules?: Prisma.CourtPriceRuleUncheckedUpdateManyWithoutCourtNestedInput
|
priceRules?: Prisma.CourtPriceRuleUncheckedUpdateManyWithoutCourtNestedInput
|
||||||
bookings?: Prisma.CourtBookingUncheckedUpdateManyWithoutCourtNestedInput
|
bookings?: Prisma.CourtBookingUncheckedUpdateManyWithoutCourtNestedInput
|
||||||
maintenances?: Prisma.CourtMaintenanceUncheckedUpdateManyWithoutCourtNestedInput
|
maintenances?: Prisma.CourtMaintenanceUncheckedUpdateManyWithoutCourtNestedInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupUncheckedUpdateManyWithoutCourtNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CourtUncheckedUpdateManyWithoutSportInput = {
|
export type CourtUncheckedUpdateManyWithoutSportInput = {
|
||||||
@@ -1242,6 +1371,7 @@ export type CourtCountOutputType = {
|
|||||||
priceRules: number
|
priceRules: number
|
||||||
bookings: number
|
bookings: number
|
||||||
maintenances: number
|
maintenances: number
|
||||||
|
recurringGroups: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CourtCountOutputTypeSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
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
|
priceRules?: boolean | CourtCountOutputTypeCountPriceRulesArgs
|
||||||
bookings?: boolean | CourtCountOutputTypeCountBookingsArgs
|
bookings?: boolean | CourtCountOutputTypeCountBookingsArgs
|
||||||
maintenances?: boolean | CourtCountOutputTypeCountMaintenancesArgs
|
maintenances?: boolean | CourtCountOutputTypeCountMaintenancesArgs
|
||||||
|
recurringGroups?: boolean | CourtCountOutputTypeCountRecurringGroupsArgs
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1289,6 +1420,13 @@ export type CourtCountOutputTypeCountMaintenancesArgs<ExtArgs extends runtime.Ty
|
|||||||
where?: Prisma.CourtMaintenanceWhereInput
|
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<{
|
export type CourtSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
|
||||||
id?: boolean
|
id?: boolean
|
||||||
@@ -1307,6 +1445,7 @@ export type CourtSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs =
|
|||||||
priceRules?: boolean | Prisma.Court$priceRulesArgs<ExtArgs>
|
priceRules?: boolean | Prisma.Court$priceRulesArgs<ExtArgs>
|
||||||
bookings?: boolean | Prisma.Court$bookingsArgs<ExtArgs>
|
bookings?: boolean | Prisma.Court$bookingsArgs<ExtArgs>
|
||||||
maintenances?: boolean | Prisma.Court$maintenancesArgs<ExtArgs>
|
maintenances?: boolean | Prisma.Court$maintenancesArgs<ExtArgs>
|
||||||
|
recurringGroups?: boolean | Prisma.Court$recurringGroupsArgs<ExtArgs>
|
||||||
_count?: boolean | Prisma.CourtCountOutputTypeDefaultArgs<ExtArgs>
|
_count?: boolean | Prisma.CourtCountOutputTypeDefaultArgs<ExtArgs>
|
||||||
}, ExtArgs["result"]["court"]>
|
}, ExtArgs["result"]["court"]>
|
||||||
|
|
||||||
@@ -1361,6 +1500,7 @@ export type CourtInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs =
|
|||||||
priceRules?: boolean | Prisma.Court$priceRulesArgs<ExtArgs>
|
priceRules?: boolean | Prisma.Court$priceRulesArgs<ExtArgs>
|
||||||
bookings?: boolean | Prisma.Court$bookingsArgs<ExtArgs>
|
bookings?: boolean | Prisma.Court$bookingsArgs<ExtArgs>
|
||||||
maintenances?: boolean | Prisma.Court$maintenancesArgs<ExtArgs>
|
maintenances?: boolean | Prisma.Court$maintenancesArgs<ExtArgs>
|
||||||
|
recurringGroups?: boolean | Prisma.Court$recurringGroupsArgs<ExtArgs>
|
||||||
_count?: boolean | Prisma.CourtCountOutputTypeDefaultArgs<ExtArgs>
|
_count?: boolean | Prisma.CourtCountOutputTypeDefaultArgs<ExtArgs>
|
||||||
}
|
}
|
||||||
export type CourtIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
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>[]
|
priceRules: Prisma.$CourtPriceRulePayload<ExtArgs>[]
|
||||||
bookings: Prisma.$CourtBookingPayload<ExtArgs>[]
|
bookings: Prisma.$CourtBookingPayload<ExtArgs>[]
|
||||||
maintenances: Prisma.$CourtMaintenancePayload<ExtArgs>[]
|
maintenances: Prisma.$CourtMaintenancePayload<ExtArgs>[]
|
||||||
|
recurringGroups: Prisma.$RecurringBookingGroupPayload<ExtArgs>[]
|
||||||
}
|
}
|
||||||
scalars: runtime.Types.Extensions.GetPayloadResult<{
|
scalars: runtime.Types.Extensions.GetPayloadResult<{
|
||||||
id: string
|
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>
|
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>
|
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>
|
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.
|
* Attaches callbacks for the resolution and/or rejection of the Promise.
|
||||||
* @param onfulfilled The callback to execute when the Promise is resolved.
|
* @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[]
|
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
|
* Court without action
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ export type CourtBookingMinAggregateOutputType = {
|
|||||||
customerPhone: string | null
|
customerPhone: string | null
|
||||||
customerEmail: string | null
|
customerEmail: string | null
|
||||||
status: $Enums.CourtBookingStatus | null
|
status: $Enums.CourtBookingStatus | null
|
||||||
|
recurringGroupId: string | null
|
||||||
createdAt: Date | null
|
createdAt: Date | null
|
||||||
updatedAt: Date | null
|
updatedAt: Date | null
|
||||||
}
|
}
|
||||||
@@ -50,6 +51,7 @@ export type CourtBookingMaxAggregateOutputType = {
|
|||||||
customerPhone: string | null
|
customerPhone: string | null
|
||||||
customerEmail: string | null
|
customerEmail: string | null
|
||||||
status: $Enums.CourtBookingStatus | null
|
status: $Enums.CourtBookingStatus | null
|
||||||
|
recurringGroupId: string | null
|
||||||
createdAt: Date | null
|
createdAt: Date | null
|
||||||
updatedAt: Date | null
|
updatedAt: Date | null
|
||||||
}
|
}
|
||||||
@@ -65,6 +67,7 @@ export type CourtBookingCountAggregateOutputType = {
|
|||||||
customerPhone: number
|
customerPhone: number
|
||||||
customerEmail: number
|
customerEmail: number
|
||||||
status: number
|
status: number
|
||||||
|
recurringGroupId: number
|
||||||
createdAt: number
|
createdAt: number
|
||||||
updatedAt: number
|
updatedAt: number
|
||||||
_all: number
|
_all: number
|
||||||
@@ -82,6 +85,7 @@ export type CourtBookingMinAggregateInputType = {
|
|||||||
customerPhone?: true
|
customerPhone?: true
|
||||||
customerEmail?: true
|
customerEmail?: true
|
||||||
status?: true
|
status?: true
|
||||||
|
recurringGroupId?: true
|
||||||
createdAt?: true
|
createdAt?: true
|
||||||
updatedAt?: true
|
updatedAt?: true
|
||||||
}
|
}
|
||||||
@@ -97,6 +101,7 @@ export type CourtBookingMaxAggregateInputType = {
|
|||||||
customerPhone?: true
|
customerPhone?: true
|
||||||
customerEmail?: true
|
customerEmail?: true
|
||||||
status?: true
|
status?: true
|
||||||
|
recurringGroupId?: true
|
||||||
createdAt?: true
|
createdAt?: true
|
||||||
updatedAt?: true
|
updatedAt?: true
|
||||||
}
|
}
|
||||||
@@ -112,6 +117,7 @@ export type CourtBookingCountAggregateInputType = {
|
|||||||
customerPhone?: true
|
customerPhone?: true
|
||||||
customerEmail?: true
|
customerEmail?: true
|
||||||
status?: true
|
status?: true
|
||||||
|
recurringGroupId?: true
|
||||||
createdAt?: true
|
createdAt?: true
|
||||||
updatedAt?: true
|
updatedAt?: true
|
||||||
_all?: true
|
_all?: true
|
||||||
@@ -200,6 +206,7 @@ export type CourtBookingGroupByOutputType = {
|
|||||||
customerPhone: string
|
customerPhone: string
|
||||||
customerEmail: string
|
customerEmail: string
|
||||||
status: $Enums.CourtBookingStatus
|
status: $Enums.CourtBookingStatus
|
||||||
|
recurringGroupId: string | null
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
updatedAt: Date
|
updatedAt: Date
|
||||||
_count: CourtBookingCountAggregateOutputType | null
|
_count: CourtBookingCountAggregateOutputType | null
|
||||||
@@ -236,9 +243,11 @@ export type CourtBookingWhereInput = {
|
|||||||
customerPhone?: Prisma.StringFilter<"CourtBooking"> | string
|
customerPhone?: Prisma.StringFilter<"CourtBooking"> | string
|
||||||
customerEmail?: Prisma.StringFilter<"CourtBooking"> | string
|
customerEmail?: Prisma.StringFilter<"CourtBooking"> | string
|
||||||
status?: Prisma.EnumCourtBookingStatusFilter<"CourtBooking"> | $Enums.CourtBookingStatus
|
status?: Prisma.EnumCourtBookingStatusFilter<"CourtBooking"> | $Enums.CourtBookingStatus
|
||||||
|
recurringGroupId?: Prisma.UuidNullableFilter<"CourtBooking"> | string | null
|
||||||
createdAt?: Prisma.DateTimeFilter<"CourtBooking"> | Date | string
|
createdAt?: Prisma.DateTimeFilter<"CourtBooking"> | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFilter<"CourtBooking"> | Date | string
|
updatedAt?: Prisma.DateTimeFilter<"CourtBooking"> | Date | string
|
||||||
court?: Prisma.XOR<Prisma.CourtScalarRelationFilter, Prisma.CourtWhereInput>
|
court?: Prisma.XOR<Prisma.CourtScalarRelationFilter, Prisma.CourtWhereInput>
|
||||||
|
recurringGroup?: Prisma.XOR<Prisma.RecurringBookingGroupNullableScalarRelationFilter, Prisma.RecurringBookingGroupWhereInput> | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CourtBookingOrderByWithRelationInput = {
|
export type CourtBookingOrderByWithRelationInput = {
|
||||||
@@ -252,9 +261,11 @@ export type CourtBookingOrderByWithRelationInput = {
|
|||||||
customerPhone?: Prisma.SortOrder
|
customerPhone?: Prisma.SortOrder
|
||||||
customerEmail?: Prisma.SortOrder
|
customerEmail?: Prisma.SortOrder
|
||||||
status?: Prisma.SortOrder
|
status?: Prisma.SortOrder
|
||||||
|
recurringGroupId?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
createdAt?: Prisma.SortOrder
|
createdAt?: Prisma.SortOrder
|
||||||
updatedAt?: Prisma.SortOrder
|
updatedAt?: Prisma.SortOrder
|
||||||
court?: Prisma.CourtOrderByWithRelationInput
|
court?: Prisma.CourtOrderByWithRelationInput
|
||||||
|
recurringGroup?: Prisma.RecurringBookingGroupOrderByWithRelationInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CourtBookingWhereUniqueInput = Prisma.AtLeast<{
|
export type CourtBookingWhereUniqueInput = Prisma.AtLeast<{
|
||||||
@@ -272,9 +283,11 @@ export type CourtBookingWhereUniqueInput = Prisma.AtLeast<{
|
|||||||
customerPhone?: Prisma.StringFilter<"CourtBooking"> | string
|
customerPhone?: Prisma.StringFilter<"CourtBooking"> | string
|
||||||
customerEmail?: Prisma.StringFilter<"CourtBooking"> | string
|
customerEmail?: Prisma.StringFilter<"CourtBooking"> | string
|
||||||
status?: Prisma.EnumCourtBookingStatusFilter<"CourtBooking"> | $Enums.CourtBookingStatus
|
status?: Prisma.EnumCourtBookingStatusFilter<"CourtBooking"> | $Enums.CourtBookingStatus
|
||||||
|
recurringGroupId?: Prisma.UuidNullableFilter<"CourtBooking"> | string | null
|
||||||
createdAt?: Prisma.DateTimeFilter<"CourtBooking"> | Date | string
|
createdAt?: Prisma.DateTimeFilter<"CourtBooking"> | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFilter<"CourtBooking"> | Date | string
|
updatedAt?: Prisma.DateTimeFilter<"CourtBooking"> | Date | string
|
||||||
court?: Prisma.XOR<Prisma.CourtScalarRelationFilter, Prisma.CourtWhereInput>
|
court?: Prisma.XOR<Prisma.CourtScalarRelationFilter, Prisma.CourtWhereInput>
|
||||||
|
recurringGroup?: Prisma.XOR<Prisma.RecurringBookingGroupNullableScalarRelationFilter, Prisma.RecurringBookingGroupWhereInput> | null
|
||||||
}, "id" | "bookingCode" | "courtId_bookingDate_startTime">
|
}, "id" | "bookingCode" | "courtId_bookingDate_startTime">
|
||||||
|
|
||||||
export type CourtBookingOrderByWithAggregationInput = {
|
export type CourtBookingOrderByWithAggregationInput = {
|
||||||
@@ -288,6 +301,7 @@ export type CourtBookingOrderByWithAggregationInput = {
|
|||||||
customerPhone?: Prisma.SortOrder
|
customerPhone?: Prisma.SortOrder
|
||||||
customerEmail?: Prisma.SortOrder
|
customerEmail?: Prisma.SortOrder
|
||||||
status?: Prisma.SortOrder
|
status?: Prisma.SortOrder
|
||||||
|
recurringGroupId?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
createdAt?: Prisma.SortOrder
|
createdAt?: Prisma.SortOrder
|
||||||
updatedAt?: Prisma.SortOrder
|
updatedAt?: Prisma.SortOrder
|
||||||
_count?: Prisma.CourtBookingCountOrderByAggregateInput
|
_count?: Prisma.CourtBookingCountOrderByAggregateInput
|
||||||
@@ -309,6 +323,7 @@ export type CourtBookingScalarWhereWithAggregatesInput = {
|
|||||||
customerPhone?: Prisma.StringWithAggregatesFilter<"CourtBooking"> | string
|
customerPhone?: Prisma.StringWithAggregatesFilter<"CourtBooking"> | string
|
||||||
customerEmail?: Prisma.StringWithAggregatesFilter<"CourtBooking"> | string
|
customerEmail?: Prisma.StringWithAggregatesFilter<"CourtBooking"> | string
|
||||||
status?: Prisma.EnumCourtBookingStatusWithAggregatesFilter<"CourtBooking"> | $Enums.CourtBookingStatus
|
status?: Prisma.EnumCourtBookingStatusWithAggregatesFilter<"CourtBooking"> | $Enums.CourtBookingStatus
|
||||||
|
recurringGroupId?: Prisma.UuidNullableWithAggregatesFilter<"CourtBooking"> | string | null
|
||||||
createdAt?: Prisma.DateTimeWithAggregatesFilter<"CourtBooking"> | Date | string
|
createdAt?: Prisma.DateTimeWithAggregatesFilter<"CourtBooking"> | Date | string
|
||||||
updatedAt?: Prisma.DateTimeWithAggregatesFilter<"CourtBooking"> | Date | string
|
updatedAt?: Prisma.DateTimeWithAggregatesFilter<"CourtBooking"> | Date | string
|
||||||
}
|
}
|
||||||
@@ -326,6 +341,7 @@ export type CourtBookingCreateInput = {
|
|||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
court: Prisma.CourtCreateNestedOneWithoutBookingsInput
|
court: Prisma.CourtCreateNestedOneWithoutBookingsInput
|
||||||
|
recurringGroup?: Prisma.RecurringBookingGroupCreateNestedOneWithoutBookingsInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CourtBookingUncheckedCreateInput = {
|
export type CourtBookingUncheckedCreateInput = {
|
||||||
@@ -339,6 +355,7 @@ export type CourtBookingUncheckedCreateInput = {
|
|||||||
customerPhone: string
|
customerPhone: string
|
||||||
customerEmail: string
|
customerEmail: string
|
||||||
status?: $Enums.CourtBookingStatus
|
status?: $Enums.CourtBookingStatus
|
||||||
|
recurringGroupId?: string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
}
|
}
|
||||||
@@ -356,6 +373,7 @@ export type CourtBookingUpdateInput = {
|
|||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
court?: Prisma.CourtUpdateOneRequiredWithoutBookingsNestedInput
|
court?: Prisma.CourtUpdateOneRequiredWithoutBookingsNestedInput
|
||||||
|
recurringGroup?: Prisma.RecurringBookingGroupUpdateOneWithoutBookingsNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CourtBookingUncheckedUpdateInput = {
|
export type CourtBookingUncheckedUpdateInput = {
|
||||||
@@ -369,6 +387,7 @@ export type CourtBookingUncheckedUpdateInput = {
|
|||||||
customerPhone?: Prisma.StringFieldUpdateOperationsInput | string
|
customerPhone?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
customerEmail?: Prisma.StringFieldUpdateOperationsInput | string
|
customerEmail?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
status?: Prisma.EnumCourtBookingStatusFieldUpdateOperationsInput | $Enums.CourtBookingStatus
|
status?: Prisma.EnumCourtBookingStatusFieldUpdateOperationsInput | $Enums.CourtBookingStatus
|
||||||
|
recurringGroupId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
}
|
}
|
||||||
@@ -384,6 +403,7 @@ export type CourtBookingCreateManyInput = {
|
|||||||
customerPhone: string
|
customerPhone: string
|
||||||
customerEmail: string
|
customerEmail: string
|
||||||
status?: $Enums.CourtBookingStatus
|
status?: $Enums.CourtBookingStatus
|
||||||
|
recurringGroupId?: string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
}
|
}
|
||||||
@@ -413,6 +433,7 @@ export type CourtBookingUncheckedUpdateManyInput = {
|
|||||||
customerPhone?: Prisma.StringFieldUpdateOperationsInput | string
|
customerPhone?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
customerEmail?: Prisma.StringFieldUpdateOperationsInput | string
|
customerEmail?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
status?: Prisma.EnumCourtBookingStatusFieldUpdateOperationsInput | $Enums.CourtBookingStatus
|
status?: Prisma.EnumCourtBookingStatusFieldUpdateOperationsInput | $Enums.CourtBookingStatus
|
||||||
|
recurringGroupId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
}
|
}
|
||||||
@@ -444,6 +465,7 @@ export type CourtBookingCountOrderByAggregateInput = {
|
|||||||
customerPhone?: Prisma.SortOrder
|
customerPhone?: Prisma.SortOrder
|
||||||
customerEmail?: Prisma.SortOrder
|
customerEmail?: Prisma.SortOrder
|
||||||
status?: Prisma.SortOrder
|
status?: Prisma.SortOrder
|
||||||
|
recurringGroupId?: Prisma.SortOrder
|
||||||
createdAt?: Prisma.SortOrder
|
createdAt?: Prisma.SortOrder
|
||||||
updatedAt?: Prisma.SortOrder
|
updatedAt?: Prisma.SortOrder
|
||||||
}
|
}
|
||||||
@@ -459,6 +481,7 @@ export type CourtBookingMaxOrderByAggregateInput = {
|
|||||||
customerPhone?: Prisma.SortOrder
|
customerPhone?: Prisma.SortOrder
|
||||||
customerEmail?: Prisma.SortOrder
|
customerEmail?: Prisma.SortOrder
|
||||||
status?: Prisma.SortOrder
|
status?: Prisma.SortOrder
|
||||||
|
recurringGroupId?: Prisma.SortOrder
|
||||||
createdAt?: Prisma.SortOrder
|
createdAt?: Prisma.SortOrder
|
||||||
updatedAt?: Prisma.SortOrder
|
updatedAt?: Prisma.SortOrder
|
||||||
}
|
}
|
||||||
@@ -474,6 +497,7 @@ export type CourtBookingMinOrderByAggregateInput = {
|
|||||||
customerPhone?: Prisma.SortOrder
|
customerPhone?: Prisma.SortOrder
|
||||||
customerEmail?: Prisma.SortOrder
|
customerEmail?: Prisma.SortOrder
|
||||||
status?: Prisma.SortOrder
|
status?: Prisma.SortOrder
|
||||||
|
recurringGroupId?: Prisma.SortOrder
|
||||||
createdAt?: Prisma.SortOrder
|
createdAt?: Prisma.SortOrder
|
||||||
updatedAt?: Prisma.SortOrder
|
updatedAt?: Prisma.SortOrder
|
||||||
}
|
}
|
||||||
@@ -524,6 +548,48 @@ export type EnumCourtBookingStatusFieldUpdateOperationsInput = {
|
|||||||
set?: $Enums.CourtBookingStatus
|
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 = {
|
export type CourtBookingCreateWithoutCourtInput = {
|
||||||
id: string
|
id: string
|
||||||
bookingCode: string
|
bookingCode: string
|
||||||
@@ -536,6 +602,7 @@ export type CourtBookingCreateWithoutCourtInput = {
|
|||||||
status?: $Enums.CourtBookingStatus
|
status?: $Enums.CourtBookingStatus
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
|
recurringGroup?: Prisma.RecurringBookingGroupCreateNestedOneWithoutBookingsInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CourtBookingUncheckedCreateWithoutCourtInput = {
|
export type CourtBookingUncheckedCreateWithoutCourtInput = {
|
||||||
@@ -548,6 +615,7 @@ export type CourtBookingUncheckedCreateWithoutCourtInput = {
|
|||||||
customerPhone: string
|
customerPhone: string
|
||||||
customerEmail: string
|
customerEmail: string
|
||||||
status?: $Enums.CourtBookingStatus
|
status?: $Enums.CourtBookingStatus
|
||||||
|
recurringGroupId?: string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
}
|
}
|
||||||
@@ -592,10 +660,67 @@ export type CourtBookingScalarWhereInput = {
|
|||||||
customerPhone?: Prisma.StringFilter<"CourtBooking"> | string
|
customerPhone?: Prisma.StringFilter<"CourtBooking"> | string
|
||||||
customerEmail?: Prisma.StringFilter<"CourtBooking"> | string
|
customerEmail?: Prisma.StringFilter<"CourtBooking"> | string
|
||||||
status?: Prisma.EnumCourtBookingStatusFilter<"CourtBooking"> | $Enums.CourtBookingStatus
|
status?: Prisma.EnumCourtBookingStatusFilter<"CourtBooking"> | $Enums.CourtBookingStatus
|
||||||
|
recurringGroupId?: Prisma.UuidNullableFilter<"CourtBooking"> | string | null
|
||||||
createdAt?: Prisma.DateTimeFilter<"CourtBooking"> | Date | string
|
createdAt?: Prisma.DateTimeFilter<"CourtBooking"> | Date | string
|
||||||
updatedAt?: 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 = {
|
export type CourtBookingCreateManyCourtInput = {
|
||||||
id: string
|
id: string
|
||||||
bookingCode: string
|
bookingCode: string
|
||||||
@@ -606,6 +731,7 @@ export type CourtBookingCreateManyCourtInput = {
|
|||||||
customerPhone: string
|
customerPhone: string
|
||||||
customerEmail: string
|
customerEmail: string
|
||||||
status?: $Enums.CourtBookingStatus
|
status?: $Enums.CourtBookingStatus
|
||||||
|
recurringGroupId?: string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
}
|
}
|
||||||
@@ -622,6 +748,7 @@ export type CourtBookingUpdateWithoutCourtInput = {
|
|||||||
status?: Prisma.EnumCourtBookingStatusFieldUpdateOperationsInput | $Enums.CourtBookingStatus
|
status?: Prisma.EnumCourtBookingStatusFieldUpdateOperationsInput | $Enums.CourtBookingStatus
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
recurringGroup?: Prisma.RecurringBookingGroupUpdateOneWithoutBookingsNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CourtBookingUncheckedUpdateWithoutCourtInput = {
|
export type CourtBookingUncheckedUpdateWithoutCourtInput = {
|
||||||
@@ -634,6 +761,7 @@ export type CourtBookingUncheckedUpdateWithoutCourtInput = {
|
|||||||
customerPhone?: Prisma.StringFieldUpdateOperationsInput | string
|
customerPhone?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
customerEmail?: Prisma.StringFieldUpdateOperationsInput | string
|
customerEmail?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
status?: Prisma.EnumCourtBookingStatusFieldUpdateOperationsInput | $Enums.CourtBookingStatus
|
status?: Prisma.EnumCourtBookingStatusFieldUpdateOperationsInput | $Enums.CourtBookingStatus
|
||||||
|
recurringGroupId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
}
|
}
|
||||||
@@ -648,6 +776,67 @@ export type CourtBookingUncheckedUpdateManyWithoutCourtInput = {
|
|||||||
customerPhone?: Prisma.StringFieldUpdateOperationsInput | string
|
customerPhone?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
customerEmail?: Prisma.StringFieldUpdateOperationsInput | string
|
customerEmail?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
status?: Prisma.EnumCourtBookingStatusFieldUpdateOperationsInput | $Enums.CourtBookingStatus
|
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
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
}
|
}
|
||||||
@@ -665,9 +854,11 @@ export type CourtBookingSelect<ExtArgs extends runtime.Types.Extensions.Internal
|
|||||||
customerPhone?: boolean
|
customerPhone?: boolean
|
||||||
customerEmail?: boolean
|
customerEmail?: boolean
|
||||||
status?: boolean
|
status?: boolean
|
||||||
|
recurringGroupId?: boolean
|
||||||
createdAt?: boolean
|
createdAt?: boolean
|
||||||
updatedAt?: boolean
|
updatedAt?: boolean
|
||||||
court?: boolean | Prisma.CourtDefaultArgs<ExtArgs>
|
court?: boolean | Prisma.CourtDefaultArgs<ExtArgs>
|
||||||
|
recurringGroup?: boolean | Prisma.CourtBooking$recurringGroupArgs<ExtArgs>
|
||||||
}, ExtArgs["result"]["courtBooking"]>
|
}, ExtArgs["result"]["courtBooking"]>
|
||||||
|
|
||||||
export type CourtBookingSelectCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
|
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
|
customerPhone?: boolean
|
||||||
customerEmail?: boolean
|
customerEmail?: boolean
|
||||||
status?: boolean
|
status?: boolean
|
||||||
|
recurringGroupId?: boolean
|
||||||
createdAt?: boolean
|
createdAt?: boolean
|
||||||
updatedAt?: boolean
|
updatedAt?: boolean
|
||||||
court?: boolean | Prisma.CourtDefaultArgs<ExtArgs>
|
court?: boolean | Prisma.CourtDefaultArgs<ExtArgs>
|
||||||
|
recurringGroup?: boolean | Prisma.CourtBooking$recurringGroupArgs<ExtArgs>
|
||||||
}, ExtArgs["result"]["courtBooking"]>
|
}, ExtArgs["result"]["courtBooking"]>
|
||||||
|
|
||||||
export type CourtBookingSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
|
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
|
customerPhone?: boolean
|
||||||
customerEmail?: boolean
|
customerEmail?: boolean
|
||||||
status?: boolean
|
status?: boolean
|
||||||
|
recurringGroupId?: boolean
|
||||||
createdAt?: boolean
|
createdAt?: boolean
|
||||||
updatedAt?: boolean
|
updatedAt?: boolean
|
||||||
court?: boolean | Prisma.CourtDefaultArgs<ExtArgs>
|
court?: boolean | Prisma.CourtDefaultArgs<ExtArgs>
|
||||||
|
recurringGroup?: boolean | Prisma.CourtBooking$recurringGroupArgs<ExtArgs>
|
||||||
}, ExtArgs["result"]["courtBooking"]>
|
}, ExtArgs["result"]["courtBooking"]>
|
||||||
|
|
||||||
export type CourtBookingSelectScalar = {
|
export type CourtBookingSelectScalar = {
|
||||||
@@ -713,25 +908,30 @@ export type CourtBookingSelectScalar = {
|
|||||||
customerPhone?: boolean
|
customerPhone?: boolean
|
||||||
customerEmail?: boolean
|
customerEmail?: boolean
|
||||||
status?: boolean
|
status?: boolean
|
||||||
|
recurringGroupId?: boolean
|
||||||
createdAt?: boolean
|
createdAt?: boolean
|
||||||
updatedAt?: boolean
|
updatedAt?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CourtBookingOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "bookingCode" | "courtId" | "bookingDate" | "startTime" | "endTime" | "customerName" | "customerPhone" | "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> = {
|
export type CourtBookingInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
court?: boolean | Prisma.CourtDefaultArgs<ExtArgs>
|
court?: boolean | Prisma.CourtDefaultArgs<ExtArgs>
|
||||||
|
recurringGroup?: boolean | Prisma.CourtBooking$recurringGroupArgs<ExtArgs>
|
||||||
}
|
}
|
||||||
export type CourtBookingIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
export type CourtBookingIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
court?: boolean | Prisma.CourtDefaultArgs<ExtArgs>
|
court?: boolean | Prisma.CourtDefaultArgs<ExtArgs>
|
||||||
|
recurringGroup?: boolean | Prisma.CourtBooking$recurringGroupArgs<ExtArgs>
|
||||||
}
|
}
|
||||||
export type CourtBookingIncludeUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
export type CourtBookingIncludeUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
court?: boolean | Prisma.CourtDefaultArgs<ExtArgs>
|
court?: boolean | Prisma.CourtDefaultArgs<ExtArgs>
|
||||||
|
recurringGroup?: boolean | Prisma.CourtBooking$recurringGroupArgs<ExtArgs>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type $CourtBookingPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
export type $CourtBookingPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
name: "CourtBooking"
|
name: "CourtBooking"
|
||||||
objects: {
|
objects: {
|
||||||
court: Prisma.$CourtPayload<ExtArgs>
|
court: Prisma.$CourtPayload<ExtArgs>
|
||||||
|
recurringGroup: Prisma.$RecurringBookingGroupPayload<ExtArgs> | null
|
||||||
}
|
}
|
||||||
scalars: runtime.Types.Extensions.GetPayloadResult<{
|
scalars: runtime.Types.Extensions.GetPayloadResult<{
|
||||||
id: string
|
id: string
|
||||||
@@ -744,6 +944,7 @@ export type $CourtBookingPayload<ExtArgs extends runtime.Types.Extensions.Intern
|
|||||||
customerPhone: string
|
customerPhone: string
|
||||||
customerEmail: string
|
customerEmail: string
|
||||||
status: $Enums.CourtBookingStatus
|
status: $Enums.CourtBookingStatus
|
||||||
|
recurringGroupId: string | null
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
updatedAt: Date
|
updatedAt: Date
|
||||||
}, ExtArgs["result"]["courtBooking"]>
|
}, 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> {
|
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"
|
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>
|
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.
|
* Attaches callbacks for the resolution and/or rejection of the Promise.
|
||||||
* @param onfulfilled The callback to execute when the Promise is resolved.
|
* @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 customerPhone: Prisma.FieldRef<"CourtBooking", 'String'>
|
||||||
readonly customerEmail: Prisma.FieldRef<"CourtBooking", 'String'>
|
readonly customerEmail: Prisma.FieldRef<"CourtBooking", 'String'>
|
||||||
readonly status: Prisma.FieldRef<"CourtBooking", 'CourtBookingStatus'>
|
readonly status: Prisma.FieldRef<"CourtBooking", 'CourtBookingStatus'>
|
||||||
|
readonly recurringGroupId: Prisma.FieldRef<"CourtBooking", 'String'>
|
||||||
readonly createdAt: Prisma.FieldRef<"CourtBooking", 'DateTime'>
|
readonly createdAt: Prisma.FieldRef<"CourtBooking", 'DateTime'>
|
||||||
readonly updatedAt: Prisma.FieldRef<"CourtBooking", 'DateTime'>
|
readonly updatedAt: Prisma.FieldRef<"CourtBooking", 'DateTime'>
|
||||||
}
|
}
|
||||||
@@ -1582,6 +1785,25 @@ export type CourtBookingDeleteManyArgs<ExtArgs extends runtime.Types.Extensions.
|
|||||||
limit?: number
|
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
|
* CourtBooking without action
|
||||||
*/
|
*/
|
||||||
|
|||||||
176
apps/backend/src/lib/booking-utils.ts
Normal file
176
apps/backend/src/lib/booking-utils.ts
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
import { randomInt } from 'node:crypto';
|
||||||
|
import type { DayOfWeek } from '@repo/api-contract';
|
||||||
|
|
||||||
|
export type Slot = {
|
||||||
|
startTime: string;
|
||||||
|
endTime: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const BOOKING_CODE_ALPHABET = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
|
||||||
|
const BOOKING_CODE_LENGTH = 6;
|
||||||
|
|
||||||
|
export const DAY_OF_WEEK_BY_INDEX: DayOfWeek[] = [
|
||||||
|
'SUNDAY',
|
||||||
|
'MONDAY',
|
||||||
|
'TUESDAY',
|
||||||
|
'WEDNESDAY',
|
||||||
|
'THURSDAY',
|
||||||
|
'FRIDAY',
|
||||||
|
'SATURDAY',
|
||||||
|
];
|
||||||
|
|
||||||
|
export function toMinutes(value: string): number {
|
||||||
|
const [hours, minutes] = value.split(':').map((part) => Number(part));
|
||||||
|
return hours * 60 + minutes;
|
||||||
|
}
|
||||||
|
|
||||||
|
export 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')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseIsoDate(date: string): { bookingDate: Date; dayOfWeek: DayOfWeek } {
|
||||||
|
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(date);
|
||||||
|
|
||||||
|
if (!match) {
|
||||||
|
throw new Error('La fecha debe tener formato YYYY-MM-DD.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const year = Number(match[1]);
|
||||||
|
const month = Number(match[2]);
|
||||||
|
const day = Number(match[3]);
|
||||||
|
|
||||||
|
const bookingDate = new Date(Date.UTC(year, month - 1, day));
|
||||||
|
|
||||||
|
if (
|
||||||
|
Number.isNaN(bookingDate.getTime()) ||
|
||||||
|
bookingDate.getUTCFullYear() !== year ||
|
||||||
|
bookingDate.getUTCMonth() + 1 !== month ||
|
||||||
|
bookingDate.getUTCDate() !== day
|
||||||
|
) {
|
||||||
|
throw new Error('La fecha enviada no es valida.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const dayOfWeek = DAY_OF_WEEK_BY_INDEX[bookingDate.getUTCDay()];
|
||||||
|
|
||||||
|
if (!dayOfWeek) {
|
||||||
|
throw new Error('No se pudo resolver el dia de la semana.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return { bookingDate, dayOfWeek };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatIsoDate(date: Date): string {
|
||||||
|
return date.toISOString().slice(0, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDayOfWeekValue(date: Date): DayOfWeek {
|
||||||
|
const dayOfWeek = DAY_OF_WEEK_BY_INDEX[date.getUTCDay()];
|
||||||
|
if (!dayOfWeek) {
|
||||||
|
throw new Error('No se pudo resolver el dia de la semana.');
|
||||||
|
}
|
||||||
|
return dayOfWeek;
|
||||||
|
}
|
||||||
|
|
||||||
|
export 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildSlots(
|
||||||
|
availability: Array<{ startTime: string; endTime: string }>,
|
||||||
|
slotDurationMinutes: number
|
||||||
|
): Slot[] {
|
||||||
|
const slots: Slot[] = [];
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasOverlap(slot: Slot, existing: Slot): boolean {
|
||||||
|
return slot.startTime < existing.endTime && slot.endTime > existing.startTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
type PriceableCourt = {
|
||||||
|
basePrice: unknown;
|
||||||
|
priceRules: Array<{
|
||||||
|
dayOfWeek: DayOfWeek | null;
|
||||||
|
startTime: string | null;
|
||||||
|
endTime: string | null;
|
||||||
|
price: unknown;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function resolveSlotPrice(court: PriceableCourt, dayOfWeek: DayOfWeek, slot: Slot): number {
|
||||||
|
const slotStart = toMinutes(slot.startTime);
|
||||||
|
const slotEnd = toMinutes(slot.endTime);
|
||||||
|
|
||||||
|
const matchingRules = court.priceRules
|
||||||
|
.filter((rule) => {
|
||||||
|
if (rule.dayOfWeek && rule.dayOfWeek !== dayOfWeek) return false;
|
||||||
|
if (!rule.startTime || !rule.endTime) return true;
|
||||||
|
return slotStart >= toMinutes(rule.startTime) && slotEnd <= toMinutes(rule.endTime);
|
||||||
|
})
|
||||||
|
.sort((first, second) => {
|
||||||
|
const firstSpecificity =
|
||||||
|
(first.dayOfWeek ? 2 : 0) + (first.startTime && first.endTime ? 1 : 0);
|
||||||
|
const secondSpecificity =
|
||||||
|
(second.dayOfWeek ? 2 : 0) + (second.startTime && second.endTime ? 1 : 0);
|
||||||
|
return secondSpecificity - firstSpecificity;
|
||||||
|
});
|
||||||
|
|
||||||
|
return Number(matchingRules[0]?.price ?? court.basePrice);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function assertAvailabilityRanges(
|
||||||
|
availability: Array<{
|
||||||
|
dayOfWeek: DayOfWeek;
|
||||||
|
startTime: string;
|
||||||
|
endTime: string;
|
||||||
|
}>
|
||||||
|
) {
|
||||||
|
const grouped = new Map<DayOfWeek, Array<{ start: number; end: number }>>();
|
||||||
|
|
||||||
|
for (const range of availability) {
|
||||||
|
const start = toMinutes(range.startTime);
|
||||||
|
const end = toMinutes(range.endTime);
|
||||||
|
|
||||||
|
if (start >= end) {
|
||||||
|
throw new Error(`El rango ${range.startTime}-${range.endTime} es invalido.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
for (let index = 1; index < ranges.length; index += 1) {
|
||||||
|
const previous = ranges[index - 1];
|
||||||
|
const current = ranges[index];
|
||||||
|
|
||||||
|
if (previous.end > current.start) {
|
||||||
|
throw new Error('Hay rangos horarios superpuestos para el mismo dia.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
41
apps/backend/src/lib/complex-access.ts
Normal file
41
apps/backend/src/lib/complex-access.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { db } from '@/lib/prisma';
|
||||||
|
|
||||||
|
export class ComplexAccessError extends Error {
|
||||||
|
status: 403 | 404;
|
||||||
|
|
||||||
|
constructor(message: string, status: 403 | 404 = 403) {
|
||||||
|
super(message);
|
||||||
|
this.name = 'ComplexAccessError';
|
||||||
|
this.status = status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export 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 ComplexAccessError('No tienes permisos para administrar este complejo.', 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
return complexUser.complex;
|
||||||
|
}
|
||||||
31
apps/backend/src/lib/slug.ts
Normal file
31
apps/backend/src/lib/slug.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { v7 as uuidv7 } from 'uuid';
|
||||||
|
|
||||||
|
export function slugify(value: string): string {
|
||||||
|
return value
|
||||||
|
.normalize('NFD')
|
||||||
|
.replace(/[\u0300-\u036f]/g, '')
|
||||||
|
.toLowerCase()
|
||||||
|
.trim()
|
||||||
|
.replace(/[^a-z0-9\s-]/g, '')
|
||||||
|
.replace(/\s+/g, '-')
|
||||||
|
.replace(/-+/g, '-');
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function buildUniqueSlug(
|
||||||
|
source: string,
|
||||||
|
findExisting: (slug: string) => Promise<{ id: string } | null>
|
||||||
|
): Promise<string> {
|
||||||
|
const base = slugify(source);
|
||||||
|
const fallback = base.length > 0 ? base : `resource-${uuidv7().slice(0, 8)}`;
|
||||||
|
|
||||||
|
let candidate = fallback;
|
||||||
|
let index = 1;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const existing = await findExisting(candidate);
|
||||||
|
if (!existing) return candidate;
|
||||||
|
|
||||||
|
index += 1;
|
||||||
|
candidate = `${fallback}-${index}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +1,21 @@
|
|||||||
import { requireAuth } from '@/middlewares/require-auth.middleware';
|
import { requireAuth } from '@/middlewares/require-auth.middleware';
|
||||||
import { createAdminBookingHandler } from '@/modules/admin-booking/handlers/create-admin-booking.handler';
|
import { cancelRecurringGroupHandler } from '@/modules/admin-booking/features/cancel-recurring-group/cancel-recurring-group.handler';
|
||||||
import { listAdminBookingsHandler } from '@/modules/admin-booking/handlers/list-admin-bookings.handler';
|
import { createAdminBookingHandler } from '@/modules/admin-booking/features/create-admin-booking/create-admin-booking.handler';
|
||||||
import { updateAdminBookingStatusHandler } from '@/modules/admin-booking/handlers/update-admin-booking-status.handler';
|
import { createAdminRecurringBookingHandler } from '@/modules/admin-booking/features/create-admin-recurring-booking/create-admin-recurring-booking.handler';
|
||||||
|
import { listAdminBookingsHandler } from '@/modules/admin-booking/features/list-admin-bookings/list-admin-bookings.handler';
|
||||||
|
import { listRecurringGroupsHandler } from '@/modules/admin-booking/features/list-recurring-groups/list-recurring-groups.handler';
|
||||||
|
import { rescheduleAdminBookingHandler } from '@/modules/admin-booking/features/reschedule-admin-booking/reschedule-admin-booking.handler';
|
||||||
|
import { updateAdminBookingStatusHandler } from '@/modules/admin-booking/features/update-admin-booking-status/update-admin-booking-status.handler';
|
||||||
|
import { updateRecurringGroupHandler } from '@/modules/admin-booking/features/update-recurring-group/update-recurring-group.handler';
|
||||||
import type { AppEnv } from '@/types/hono';
|
import type { AppEnv } from '@/types/hono';
|
||||||
import { zValidator } from '@hono/zod-validator';
|
import { zValidator } from '@hono/zod-validator';
|
||||||
import {
|
import {
|
||||||
createAdminBookingSchema,
|
createAdminBookingSchema,
|
||||||
|
createRecurringBookingSchema,
|
||||||
listAdminBookingsQuerySchema,
|
listAdminBookingsQuerySchema,
|
||||||
|
rescheduleAdminBookingSchema,
|
||||||
updateAdminBookingStatusSchema,
|
updateAdminBookingStatusSchema,
|
||||||
|
updateRecurringGroupSchema,
|
||||||
} from '@repo/api-contract';
|
} from '@repo/api-contract';
|
||||||
import { Hono } from 'hono';
|
import { Hono } from 'hono';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
@@ -15,6 +23,7 @@ import { z } from 'zod';
|
|||||||
export const adminBookingRoutes = new Hono<AppEnv>();
|
export const adminBookingRoutes = new Hono<AppEnv>();
|
||||||
const complexIdParamsSchema = z.object({ complexId: z.uuid() });
|
const complexIdParamsSchema = z.object({ complexId: z.uuid() });
|
||||||
const bookingIdParamsSchema = z.object({ id: z.uuid() });
|
const bookingIdParamsSchema = z.object({ id: z.uuid() });
|
||||||
|
const groupIdParamsSchema = z.object({ groupId: z.uuid() });
|
||||||
|
|
||||||
adminBookingRoutes.use('*', requireAuth);
|
adminBookingRoutes.use('*', requireAuth);
|
||||||
|
|
||||||
@@ -32,9 +41,42 @@ adminBookingRoutes.post(
|
|||||||
createAdminBookingHandler
|
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(
|
adminBookingRoutes.patch(
|
||||||
'/:id/status',
|
'/:id/status',
|
||||||
zValidator('param', bookingIdParamsSchema),
|
zValidator('param', bookingIdParamsSchema),
|
||||||
zValidator('json', updateAdminBookingStatusSchema),
|
zValidator('json', updateAdminBookingStatusSchema),
|
||||||
updateAdminBookingStatusHandler
|
updateAdminBookingStatusHandler
|
||||||
);
|
);
|
||||||
|
|
||||||
|
adminBookingRoutes.patch(
|
||||||
|
'/:id/reschedule',
|
||||||
|
zValidator('param', bookingIdParamsSchema),
|
||||||
|
zValidator('json', rescheduleAdminBookingSchema),
|
||||||
|
rescheduleAdminBookingHandler
|
||||||
|
);
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import { Errors } from '@/lib/errors';
|
||||||
|
import { db } from '@/lib/prisma';
|
||||||
|
import type { Result } from '@/lib/result';
|
||||||
|
import { err, ok } from '@/lib/result';
|
||||||
|
import { v7 as uuidv7 } from 'uuid';
|
||||||
|
|
||||||
|
export async function cancelRecurringGroup(
|
||||||
|
userId: string,
|
||||||
|
groupId: string
|
||||||
|
): Promise<Result<{ ok: boolean }>> {
|
||||||
|
const group = await db.recurringBookingGroup.findUnique({
|
||||||
|
where: { id: groupId },
|
||||||
|
include: {
|
||||||
|
complex: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
users: { where: { userId }, select: { userId: true } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!group) {
|
||||||
|
return err(Errors.notFound('Grupo de reservas no encontrado.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (group.complex.users.length === 0) {
|
||||||
|
return err(Errors.forbidden('No tienes permisos para administrar este complejo.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (group.status === 'CANCELLED') {
|
||||||
|
return err(Errors.conflict('El grupo ya fue cancelado anteriormente.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
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({ ok: true });
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { handleResult } from '@/lib/http/handle-result';
|
||||||
|
import type { AppContext } from '@/types/hono';
|
||||||
|
import { cancelRecurringGroup } from './cancel-recurring-group.business';
|
||||||
|
|
||||||
|
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');
|
||||||
|
|
||||||
|
return handleResult(c, await cancelRecurringGroup(user.id, groupId));
|
||||||
|
}
|
||||||
@@ -0,0 +1,187 @@
|
|||||||
|
import { randomInt } from 'node:crypto';
|
||||||
|
import { Errors } from '@/lib/errors';
|
||||||
|
import { db } from '@/lib/prisma';
|
||||||
|
import type { Result } from '@/lib/result';
|
||||||
|
import { err, ok } from '@/lib/result';
|
||||||
|
import { isSlotInPast } from '@/lib/slot-validator';
|
||||||
|
import { evaluatePlanUsage, parsePlanRules } from '@/modules/plan/services/plan-rules.service';
|
||||||
|
import type { AdminBooking, CreateAdminBookingInput } from '@repo/api-contract';
|
||||||
|
import { v7 as uuidv7 } from 'uuid';
|
||||||
|
import { AdminBookingServiceError } from '../../shared/errors';
|
||||||
|
import {
|
||||||
|
buildSlots,
|
||||||
|
ensureComplexAccess,
|
||||||
|
getDayOfWeek,
|
||||||
|
mapBookingResponse,
|
||||||
|
minutesToTime,
|
||||||
|
parseIsoDate,
|
||||||
|
resolvePrice,
|
||||||
|
toMinutes,
|
||||||
|
} from '../../shared/helpers';
|
||||||
|
|
||||||
|
const BOOKING_CODE_ALPHABET = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
|
||||||
|
const BOOKING_CODE_LENGTH = 6;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createAdminBooking(
|
||||||
|
userId: string,
|
||||||
|
complexId: string,
|
||||||
|
input: CreateAdminBookingInput
|
||||||
|
): Promise<Result<AdminBooking>> {
|
||||||
|
const complexResult = await ensureComplexAccess(complexId, userId);
|
||||||
|
if (!complexResult.ok) return err(complexResult.error);
|
||||||
|
const complex = complexResult.value;
|
||||||
|
|
||||||
|
const adminUser = await db.user.findUnique({
|
||||||
|
where: { id: userId },
|
||||||
|
select: { emailVerified: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!adminUser?.emailVerified) {
|
||||||
|
return err(Errors.forbidden('Debés verificar tu email para poder crear reservas.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const dateResult = parseIsoDate(input.date);
|
||||||
|
if (!dateResult.ok) return err(dateResult.error);
|
||||||
|
const bookingDate = dateResult.value;
|
||||||
|
|
||||||
|
const dayOfWeekResult = getDayOfWeek(bookingDate);
|
||||||
|
if (!dayOfWeekResult.ok) return err(dayOfWeekResult.error);
|
||||||
|
const dayOfWeek = dayOfWeekResult.value;
|
||||||
|
|
||||||
|
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) {
|
||||||
|
return err(Errors.notFound('La cancha seleccionada no existe en el complejo.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const validSlots = buildSlots(court.availabilities, court.slotDurationMinutes);
|
||||||
|
const selectedStartMinutes = toMinutes(input.startTime);
|
||||||
|
const selectedEndMinutes = selectedStartMinutes + 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) {
|
||||||
|
return err(Errors.conflict('El horario seleccionado no esta disponible para esa cancha.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSlotInPast(bookingDate, input.startTime, court.slotDurationMinutes)) {
|
||||||
|
return err(Errors.validation('No se pueden crear reservas en el pasado.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let attempt = 0; attempt < 5; attempt += 1) {
|
||||||
|
try {
|
||||||
|
const booking = await db.$transaction(async (tx) => {
|
||||||
|
if (complex.plan) {
|
||||||
|
const rules = parsePlanRules(complex.plan.rules);
|
||||||
|
const bookingsForDate = await tx.courtBooking.count({
|
||||||
|
where: {
|
||||||
|
bookingDate,
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const overlappingBooking = await tx.courtBooking.findFirst({
|
||||||
|
where: {
|
||||||
|
courtId: court.id,
|
||||||
|
bookingDate,
|
||||||
|
status: 'CONFIRMED',
|
||||||
|
startTime: { lt: selectedSlot.endTime },
|
||||||
|
endTime: { gt: selectedSlot.startTime },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (overlappingBooking) {
|
||||||
|
throw new AdminBookingServiceError('El horario seleccionado ya fue reservado.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return tx.courtBooking.create({
|
||||||
|
data: {
|
||||||
|
id: uuidv7(),
|
||||||
|
bookingCode: generateBookingCode(),
|
||||||
|
courtId: court.id,
|
||||||
|
bookingDate,
|
||||||
|
startTime: selectedSlot.startTime,
|
||||||
|
endTime: selectedSlot.endTime,
|
||||||
|
customerName: input.customerName.trim(),
|
||||||
|
customerPhone: input.customerPhone.trim(),
|
||||||
|
customerEmail: input.customerEmail?.trim() ?? '',
|
||||||
|
status: 'CONFIRMED',
|
||||||
|
recurringGroupId: null,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
court: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
sport: { select: { id: true, name: true, slug: true } },
|
||||||
|
complex: { select: { id: true, complexName: true } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const price = resolvePrice(court, dayOfWeek, selectedSlot.startTime, selectedSlot.endTime);
|
||||||
|
return ok(mapBookingResponse({ ...booking, price }));
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof AdminBookingServiceError) {
|
||||||
|
return err(Errors.conflict(error.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
return err(Errors.conflict('El horario seleccionado ya fue reservado.'));
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return err(Errors.conflict('No se pudo generar un codigo de reserva unico. Intenta nuevamente.'));
|
||||||
|
}
|
||||||
@@ -1,11 +1,9 @@
|
|||||||
|
import { handleResult } from '@/lib/http/handle-result';
|
||||||
import { db } from '@/lib/prisma';
|
import { db } from '@/lib/prisma';
|
||||||
import {
|
|
||||||
AdminBookingServiceError,
|
|
||||||
createAdminBooking,
|
|
||||||
} from '@/modules/admin-booking/services/admin-booking.service';
|
|
||||||
import { sendBookingConfirmation } from '@/services/booking-email.service';
|
import { sendBookingConfirmation } from '@/services/booking-email.service';
|
||||||
import type { AppContext } from '@/types/hono';
|
import type { AppContext } from '@/types/hono';
|
||||||
import type { CreateAdminBookingInput } from '@repo/api-contract';
|
import type { CreateAdminBookingInput } from '@repo/api-contract';
|
||||||
|
import { createAdminBooking } from './create-admin-booking.business';
|
||||||
|
|
||||||
type ComplexIdParams = { complexId: string };
|
type ComplexIdParams = { complexId: string };
|
||||||
|
|
||||||
@@ -14,8 +12,10 @@ export async function createAdminBookingHandler(c: AppContext) {
|
|||||||
const payload = c.req.valid('json' as never) as CreateAdminBookingInput;
|
const payload = c.req.valid('json' as never) as CreateAdminBookingInput;
|
||||||
const user = c.get('user');
|
const user = c.get('user');
|
||||||
|
|
||||||
try {
|
const result = await createAdminBooking(user.id, complexId, payload);
|
||||||
const booking = await createAdminBooking(user.id, complexId, payload);
|
|
||||||
|
if (result.ok) {
|
||||||
|
const booking = result.value;
|
||||||
|
|
||||||
const complex = await db.complex.findUnique({
|
const complex = await db.complex.findUnique({
|
||||||
where: { id: complexId },
|
where: { id: complexId },
|
||||||
@@ -35,12 +35,7 @@ export async function createAdminBookingHandler(c: AppContext) {
|
|||||||
customerEmail: booking.customerEmail,
|
customerEmail: booking.customerEmail,
|
||||||
price: booking.price,
|
price: booking.price,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return c.json(booking, 201);
|
return handleResult(c, result, 201);
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof AdminBookingServiceError) {
|
|
||||||
return c.json({ message: error.message }, error.status);
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,329 @@
|
|||||||
|
import { randomInt } from 'node:crypto';
|
||||||
|
import { DayOfWeek as DayOfWeekEnum } from '@/generated/prisma/enums';
|
||||||
|
import { Errors } from '@/lib/errors';
|
||||||
|
import { db } from '@/lib/prisma';
|
||||||
|
import type { Result } from '@/lib/result';
|
||||||
|
import { err, ok } from '@/lib/result';
|
||||||
|
import { isSlotInPast } from '@/lib/slot-validator';
|
||||||
|
import {
|
||||||
|
evaluatePlanUsage,
|
||||||
|
isFeatureEnabled,
|
||||||
|
parsePlanRules,
|
||||||
|
} from '@/modules/plan/services/plan-rules.service';
|
||||||
|
import type { CreateRecurringBookingInput, RecurringBookingGroup } from '@repo/api-contract';
|
||||||
|
import { v7 as uuidv7 } from 'uuid';
|
||||||
|
import { AdminBookingServiceError } from '../../shared/errors';
|
||||||
|
import {
|
||||||
|
buildSlots,
|
||||||
|
ensureComplexAccess,
|
||||||
|
formatIsoDate,
|
||||||
|
minutesToTime,
|
||||||
|
parseIsoDate,
|
||||||
|
toMinutes,
|
||||||
|
} from '../../shared/helpers';
|
||||||
|
|
||||||
|
const BOOKING_CODE_ALPHABET = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
|
||||||
|
const BOOKING_CODE_LENGTH = 6;
|
||||||
|
const MAX_RECURRING_WEEKS = 52;
|
||||||
|
|
||||||
|
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 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* 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 getDayOfWeekValue(date: Date): Result<DayOfWeek> {
|
||||||
|
const dayOfWeek = DAY_OF_WEEK_BY_INDEX[date.getUTCDay()];
|
||||||
|
if (!dayOfWeek) {
|
||||||
|
return err(Errors.validation('No se pudo resolver el dia de la semana.'));
|
||||||
|
}
|
||||||
|
return ok(dayOfWeek);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createAdminRecurringBooking(
|
||||||
|
userId: string,
|
||||||
|
complexId: string,
|
||||||
|
input: CreateRecurringBookingInput
|
||||||
|
): Promise<Result<RecurringBookingGroup>> {
|
||||||
|
const complexResult = await ensureComplexAccess(complexId, userId);
|
||||||
|
if (!complexResult.ok) return err(complexResult.error);
|
||||||
|
const complex = complexResult.value;
|
||||||
|
|
||||||
|
const adminUser = await db.user.findUnique({
|
||||||
|
where: { id: userId },
|
||||||
|
select: { emailVerified: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!adminUser?.emailVerified) {
|
||||||
|
return err(Errors.forbidden('Debés verificar tu email para poder crear reservas.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!complex.plan) {
|
||||||
|
return err(Errors.forbidden('El complejo no tiene un plan asignado.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const rules = parsePlanRules(complex.plan.rules);
|
||||||
|
|
||||||
|
if (!isFeatureEnabled(rules, 'fixedSlots')) {
|
||||||
|
return err(
|
||||||
|
Errors.forbidden(
|
||||||
|
'Tu plan no permite la creación de turnos fijos. Comunicate con el administrador.'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const startDateResult = parseIsoDate(input.date);
|
||||||
|
if (!startDateResult.ok) return err(startDateResult.error);
|
||||||
|
const startDate = startDateResult.value;
|
||||||
|
|
||||||
|
const dayOfWeekResult = getDayOfWeekValue(startDate);
|
||||||
|
if (!dayOfWeekResult.ok) return err(dayOfWeekResult.error);
|
||||||
|
const dayOfWeek = dayOfWeekResult.value;
|
||||||
|
|
||||||
|
const endDateResult = input.recurringEndDate ? parseIsoDate(input.recurringEndDate) : null;
|
||||||
|
if (endDateResult && !endDateResult.ok) return err(endDateResult.error);
|
||||||
|
const endDate = endDateResult?.value ?? null;
|
||||||
|
|
||||||
|
if (endDate && endDate <= startDate) {
|
||||||
|
return err(Errors.validation('La fecha de fin debe ser posterior a la fecha de inicio.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
return err(Errors.notFound('La cancha seleccionada no existe en el complejo.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
return err(Errors.conflict('El horario seleccionado no esta disponible para esa cancha.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSlotInPast(startDate, input.startTime, court.slotDurationMinutes)) {
|
||||||
|
return err(Errors.validation('La fecha de inicio no puede estar en el pasado.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const recurringDates = Array.from(
|
||||||
|
generateRecurringDates(startDate, endDate, startDate.getUTCDay())
|
||||||
|
);
|
||||||
|
|
||||||
|
if (recurringDates.length === 0) {
|
||||||
|
return err(
|
||||||
|
Errors.validation(
|
||||||
|
'No se generaron fechas para la reserva periódica. Verifica las fechas ingresadas.'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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)}.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 ok({
|
||||||
|
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) => ({
|
||||||
|
id: b.id,
|
||||||
|
bookingCode: b.bookingCode,
|
||||||
|
complexId: b.court.complex.id,
|
||||||
|
complexName: b.court.complex.complexName,
|
||||||
|
courtId: b.court.id,
|
||||||
|
courtName: b.court.name,
|
||||||
|
sport: { id: b.court.sport.id, name: b.court.sport.name, slug: b.court.sport.slug },
|
||||||
|
date: formatIsoDate(b.bookingDate),
|
||||||
|
startTime: b.startTime,
|
||||||
|
endTime: b.endTime,
|
||||||
|
customerName: b.customerName,
|
||||||
|
customerPhone: b.customerPhone,
|
||||||
|
customerEmail: b.customerEmail,
|
||||||
|
price: 0,
|
||||||
|
status: b.status as 'CONFIRMED',
|
||||||
|
recurringGroupId: groupId,
|
||||||
|
createdAt: b.createdAt.toISOString(),
|
||||||
|
updatedAt: b.updatedAt.toISOString(),
|
||||||
|
})),
|
||||||
|
createdAt: result.group.createdAt.toISOString(),
|
||||||
|
updatedAt: result.group.updatedAt.toISOString(),
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof AdminBookingServiceError) {
|
||||||
|
return err(Errors.conflict(error.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
return err(Errors.conflict('El horario seleccionado ya fue reservado.'));
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return err(Errors.conflict('No se pudo generar un codigo de reserva unico. Intenta nuevamente.'));
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { handleResult } from '@/lib/http/handle-result';
|
||||||
|
import { sendBookingConfirmation } from '@/services/booking-email.service';
|
||||||
|
import type { AppContext } from '@/types/hono';
|
||||||
|
import type { CreateRecurringBookingInput } from '@repo/api-contract';
|
||||||
|
import { createAdminRecurringBooking } from './create-admin-recurring-booking.business';
|
||||||
|
|
||||||
|
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');
|
||||||
|
|
||||||
|
const result = await createAdminRecurringBooking(user.id, complexId, payload);
|
||||||
|
|
||||||
|
if (result.ok) {
|
||||||
|
const group = result.value;
|
||||||
|
|
||||||
|
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 handleResult(c, result, 201);
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { db } from '@/lib/prisma';
|
||||||
|
import type { Result } from '@/lib/result';
|
||||||
|
import { err, ok } from '@/lib/result';
|
||||||
|
import type { ListAdminBookingsQuery } from '@repo/api-contract';
|
||||||
|
import { ensureComplexAccess, mapBookingResponse, parseIsoDate } from '../../shared/helpers';
|
||||||
|
|
||||||
|
export async function listAdminBookings(
|
||||||
|
userId: string,
|
||||||
|
complexId: string,
|
||||||
|
query: ListAdminBookingsQuery
|
||||||
|
): Promise<Result<{ bookings: unknown[] }>> {
|
||||||
|
const accessResult = await ensureComplexAccess(complexId, userId);
|
||||||
|
if (!accessResult.ok) return err(accessResult.error);
|
||||||
|
|
||||||
|
const dateResult = parseIsoDate(query.fromDate);
|
||||||
|
if (!dateResult.ok) return err(dateResult.error);
|
||||||
|
const fromDate = dateResult.value;
|
||||||
|
|
||||||
|
const bookings = await db.courtBooking.findMany({
|
||||||
|
where: {
|
||||||
|
bookingDate: { gte: fromDate },
|
||||||
|
court: { complexId },
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
court: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
sport: { select: { id: true, name: true, slug: true } },
|
||||||
|
complex: { select: { id: true, complexName: true } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
orderBy: [{ bookingDate: 'asc' }, { startTime: 'asc' }, { createdAt: 'asc' }],
|
||||||
|
});
|
||||||
|
|
||||||
|
return ok({ bookings: bookings.map((b) => mapBookingResponse(b)) });
|
||||||
|
}
|
||||||
@@ -1,9 +1,7 @@
|
|||||||
import {
|
import { handleResult } from '@/lib/http/handle-result';
|
||||||
AdminBookingServiceError,
|
|
||||||
listAdminBookings,
|
|
||||||
} from '@/modules/admin-booking/services/admin-booking.service';
|
|
||||||
import type { AppContext } from '@/types/hono';
|
import type { AppContext } from '@/types/hono';
|
||||||
import type { ListAdminBookingsQuery } from '@repo/api-contract';
|
import type { ListAdminBookingsQuery } from '@repo/api-contract';
|
||||||
|
import { listAdminBookings } from './list-admin-bookings.business';
|
||||||
|
|
||||||
type ComplexIdParams = { complexId: string };
|
type ComplexIdParams = { complexId: string };
|
||||||
|
|
||||||
@@ -12,13 +10,5 @@ export async function listAdminBookingsHandler(c: AppContext) {
|
|||||||
const query = c.req.valid('query' as never) as ListAdminBookingsQuery;
|
const query = c.req.valid('query' as never) as ListAdminBookingsQuery;
|
||||||
const user = c.get('user');
|
const user = c.get('user');
|
||||||
|
|
||||||
try {
|
return handleResult(c, await listAdminBookings(user.id, complexId, query));
|
||||||
const response = await listAdminBookings(user.id, complexId, query);
|
|
||||||
return c.json(response);
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof AdminBookingServiceError) {
|
|
||||||
return c.json({ message: error.message }, error.status);
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
import { db } from '@/lib/prisma';
|
||||||
|
import type { Result } from '@/lib/result';
|
||||||
|
import { ok } from '@/lib/result';
|
||||||
|
import { formatIsoDate } from '../../shared/helpers';
|
||||||
|
|
||||||
|
export async function listRecurringGroups(complexId: string): Promise<
|
||||||
|
Result<
|
||||||
|
Array<{
|
||||||
|
id: string;
|
||||||
|
complexId: string;
|
||||||
|
courtId: string;
|
||||||
|
startTime: string;
|
||||||
|
endTime: string;
|
||||||
|
dayOfWeek: string;
|
||||||
|
startDate: string;
|
||||||
|
endDate: string | null;
|
||||||
|
status: string;
|
||||||
|
customerName: string;
|
||||||
|
customerPhone: string;
|
||||||
|
customerEmail: string;
|
||||||
|
bookings: Array<{
|
||||||
|
id: string;
|
||||||
|
bookingCode: string;
|
||||||
|
complexId: string;
|
||||||
|
complexName: string;
|
||||||
|
courtId: string;
|
||||||
|
courtName: string;
|
||||||
|
sport: { id: string; name: string; slug: string };
|
||||||
|
date: string;
|
||||||
|
startTime: string;
|
||||||
|
endTime: string;
|
||||||
|
customerName: string;
|
||||||
|
customerPhone: string;
|
||||||
|
customerEmail: string;
|
||||||
|
price: number;
|
||||||
|
status: string;
|
||||||
|
recurringGroupId: string | null;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}>;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}>
|
||||||
|
>
|
||||||
|
> {
|
||||||
|
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 ok(
|
||||||
|
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((b) => ({
|
||||||
|
id: b.id,
|
||||||
|
bookingCode: b.bookingCode,
|
||||||
|
complexId: b.court.complex.id,
|
||||||
|
complexName: b.court.complex.complexName,
|
||||||
|
courtId: b.court.id,
|
||||||
|
courtName: b.court.name,
|
||||||
|
sport: { id: b.court.sport.id, name: b.court.sport.name, slug: b.court.sport.slug },
|
||||||
|
date: formatIsoDate(b.bookingDate),
|
||||||
|
startTime: b.startTime,
|
||||||
|
endTime: b.endTime,
|
||||||
|
customerName: b.customerName,
|
||||||
|
customerPhone: b.customerPhone,
|
||||||
|
customerEmail: b.customerEmail,
|
||||||
|
price: 0,
|
||||||
|
status: b.status,
|
||||||
|
recurringGroupId: b.recurringGroupId,
|
||||||
|
createdAt: b.createdAt.toISOString(),
|
||||||
|
updatedAt: b.updatedAt.toISOString(),
|
||||||
|
})),
|
||||||
|
createdAt: group.createdAt.toISOString(),
|
||||||
|
updatedAt: group.updatedAt.toISOString(),
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { handleResult } from '@/lib/http/handle-result';
|
||||||
|
import type { Result } from '@/lib/result';
|
||||||
|
import type { AppContext } from '@/types/hono';
|
||||||
|
import { listRecurringGroups } from './list-recurring-groups.business';
|
||||||
|
|
||||||
|
type ComplexIdParams = { complexId: string };
|
||||||
|
|
||||||
|
export async function listRecurringGroupsHandler(c: AppContext) {
|
||||||
|
const { complexId } = c.req.valid('param' as never) as ComplexIdParams;
|
||||||
|
|
||||||
|
const result = await listRecurringGroups(complexId);
|
||||||
|
return handleResult(
|
||||||
|
c,
|
||||||
|
result.ok
|
||||||
|
? ({ ok: true as const, value: { groups: result.value } } as Result<{ groups: unknown }>)
|
||||||
|
: result
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
import { Errors } from '@/lib/errors';
|
||||||
|
import { db } from '@/lib/prisma';
|
||||||
|
import type { Result } from '@/lib/result';
|
||||||
|
import { err, ok } from '@/lib/result';
|
||||||
|
import { isSlotInPast } from '@/lib/slot-validator';
|
||||||
|
import type { AdminBooking, RescheduleAdminBookingInput } from '@repo/api-contract';
|
||||||
|
import { v7 as uuidv7 } from 'uuid';
|
||||||
|
import {
|
||||||
|
buildSlots,
|
||||||
|
getDayOfWeek,
|
||||||
|
mapBookingResponse,
|
||||||
|
minutesToTime,
|
||||||
|
resolvePrice,
|
||||||
|
toMinutes,
|
||||||
|
} from '../../shared/helpers';
|
||||||
|
|
||||||
|
export async function rescheduleAdminBooking(
|
||||||
|
userId: string,
|
||||||
|
bookingId: string,
|
||||||
|
input: RescheduleAdminBookingInput
|
||||||
|
): Promise<Result<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) {
|
||||||
|
return err(Errors.notFound('Reserva no encontrada.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (booking.status !== 'CONFIRMED') {
|
||||||
|
return err(Errors.conflict('Solo se pueden reprogramar reservas en estado confirmada.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetCourtId = input.courtId ?? booking.courtId;
|
||||||
|
const targetStartTime = input.startTime ?? booking.startTime;
|
||||||
|
|
||||||
|
if (targetCourtId === booking.courtId && targetStartTime === booking.startTime) {
|
||||||
|
return err(Errors.validation('Debe proporcionar al menos una cancha o un horario diferente.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const dayOfWeekResult = getDayOfWeek(booking.bookingDate);
|
||||||
|
if (!dayOfWeekResult.ok) return err(dayOfWeekResult.error);
|
||||||
|
const dayOfWeek = dayOfWeekResult.value;
|
||||||
|
|
||||||
|
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) {
|
||||||
|
return err(Errors.notFound('La cancha seleccionada no existe en el complejo.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetCourt.sport.id !== booking.court.sport.id) {
|
||||||
|
return err(
|
||||||
|
Errors.conflict('La cancha seleccionada no es del mismo deporte que la reserva original.')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetCourt.isUnderMaintenance) {
|
||||||
|
return err(Errors.conflict('La cancha seleccionada se encuentra en mantenimiento.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
return err(Errors.conflict('El horario seleccionado no está disponible para esa cancha.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSlotInPast(booking.bookingDate, targetStartTime, targetCourt.slotDurationMinutes)) {
|
||||||
|
return err(Errors.validation('No se pueden reprogramar reservas en el pasado.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
return err(Errors.conflict('El horario seleccionado ya fue reservado por otra reserva.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
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 ok(mapBookingResponse(updatedBooking));
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { handleResult } from '@/lib/http/handle-result';
|
||||||
|
import { db } from '@/lib/prisma';
|
||||||
|
import { sseManager } from '@/lib/sse';
|
||||||
|
import { sendBookingRescheduled } from '@/services/booking-email.service';
|
||||||
|
import type { AppContext } from '@/types/hono';
|
||||||
|
import type { RescheduleAdminBookingInput } from '@repo/api-contract';
|
||||||
|
import { rescheduleAdminBooking } from './reschedule-admin-booking.business';
|
||||||
|
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await rescheduleAdminBooking(user.id, id, payload);
|
||||||
|
|
||||||
|
if (result.ok) {
|
||||||
|
const booking = result.value;
|
||||||
|
|
||||||
|
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 handleResult(c, result);
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import { Errors } from '@/lib/errors';
|
||||||
|
import { db } from '@/lib/prisma';
|
||||||
|
import type { Result } from '@/lib/result';
|
||||||
|
import { err, ok } from '@/lib/result';
|
||||||
|
import type { UpdateAdminBookingStatusInput } from '@repo/api-contract';
|
||||||
|
import { v7 as uuidv7 } from 'uuid';
|
||||||
|
import { mapBookingResponse } from '../../shared/helpers';
|
||||||
|
|
||||||
|
export async function updateAdminBookingStatus(
|
||||||
|
userId: string,
|
||||||
|
bookingId: string,
|
||||||
|
input: UpdateAdminBookingStatusInput
|
||||||
|
): Promise<Result<ReturnType<typeof mapBookingResponse>>> {
|
||||||
|
const booking = await db.courtBooking.findFirst({
|
||||||
|
where: {
|
||||||
|
id: bookingId,
|
||||||
|
court: {
|
||||||
|
complex: { users: { some: { userId } } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
court: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
sport: { select: { id: true, name: true, slug: true } },
|
||||||
|
complex: { select: { id: true, complexName: true } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!booking) {
|
||||||
|
return err(Errors.notFound('Reserva no encontrada.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.status === 'CANCELLED' && booking.status !== 'CONFIRMED') {
|
||||||
|
return err(Errors.conflict('Solo se pueden cancelar reservas en estado confirmada.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.status === 'COMPLETED' && booking.status !== 'CONFIRMED') {
|
||||||
|
return err(Errors.conflict('Solo se pueden marcar como cumplidas las reservas confirmadas.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.status === 'NOSHOW' && booking.status !== 'CONFIRMED') {
|
||||||
|
return err(Errors.conflict('Solo se pueden marcar como no show las reservas confirmadas.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.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: input.status,
|
||||||
|
changedAt: new Date(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (input.status === 'COMPLETED' || input.status === 'NOSHOW') {
|
||||||
|
await db.courtBooking.update({
|
||||||
|
where: { id: booking.id },
|
||||||
|
data: { status: input.status },
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await db.courtBooking.delete({ where: { id: booking.id } });
|
||||||
|
}
|
||||||
|
|
||||||
|
return ok(mapBookingResponse({ ...booking, status: input.status }));
|
||||||
|
}
|
||||||
@@ -1,10 +1,8 @@
|
|||||||
import {
|
import { handleResult } from '@/lib/http/handle-result';
|
||||||
AdminBookingServiceError,
|
|
||||||
updateAdminBookingStatus,
|
|
||||||
} from '@/modules/admin-booking/services/admin-booking.service';
|
|
||||||
import { sendBookingCancelled, sendBookingNoShow } from '@/services/booking-email.service';
|
import { sendBookingCancelled, sendBookingNoShow } from '@/services/booking-email.service';
|
||||||
import type { AppContext } from '@/types/hono';
|
import type { AppContext } from '@/types/hono';
|
||||||
import type { UpdateAdminBookingStatusInput } from '@repo/api-contract';
|
import type { UpdateAdminBookingStatusInput } from '@repo/api-contract';
|
||||||
|
import { updateAdminBookingStatus } from './update-admin-booking-status.business';
|
||||||
|
|
||||||
type BookingIdParams = { id: string };
|
type BookingIdParams = { id: string };
|
||||||
|
|
||||||
@@ -13,8 +11,10 @@ export async function updateAdminBookingStatusHandler(c: AppContext) {
|
|||||||
const payload = c.req.valid('json' as never) as UpdateAdminBookingStatusInput;
|
const payload = c.req.valid('json' as never) as UpdateAdminBookingStatusInput;
|
||||||
const user = c.get('user');
|
const user = c.get('user');
|
||||||
|
|
||||||
try {
|
const result = await updateAdminBookingStatus(user.id, id, payload);
|
||||||
const booking = await updateAdminBookingStatus(user.id, id, payload);
|
|
||||||
|
if (result.ok) {
|
||||||
|
const booking = result.value;
|
||||||
|
|
||||||
if (payload.status === 'CANCELLED') {
|
if (payload.status === 'CANCELLED') {
|
||||||
void sendBookingCancelled({
|
void sendBookingCancelled({
|
||||||
@@ -41,12 +41,7 @@ export async function updateAdminBookingStatusHandler(c: AppContext) {
|
|||||||
customerEmail: booking.customerEmail,
|
customerEmail: booking.customerEmail,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return c.json(booking);
|
return handleResult(c, result);
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof AdminBookingServiceError) {
|
|
||||||
return c.json({ message: error.message }, error.status);
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,318 @@
|
|||||||
|
import { randomInt } from 'node:crypto';
|
||||||
|
import { Errors } from '@/lib/errors';
|
||||||
|
import { db } from '@/lib/prisma';
|
||||||
|
import type { Result } from '@/lib/result';
|
||||||
|
import { err, ok } from '@/lib/result';
|
||||||
|
import { isSlotInPast } from '@/lib/slot-validator';
|
||||||
|
import type { RecurringBookingGroup, UpdateRecurringGroupInput } from '@repo/api-contract';
|
||||||
|
import { v7 as uuidv7 } from 'uuid';
|
||||||
|
import { AdminBookingServiceError } from '../../shared/errors';
|
||||||
|
import { buildSlots, formatIsoDate, minutesToTime, toMinutes } from '../../shared/helpers';
|
||||||
|
|
||||||
|
const DAY_INDEX_BY_VALUE: Record<string, number> = {
|
||||||
|
SUNDAY: 0,
|
||||||
|
MONDAY: 1,
|
||||||
|
TUESDAY: 2,
|
||||||
|
WEDNESDAY: 3,
|
||||||
|
THURSDAY: 4,
|
||||||
|
FRIDAY: 5,
|
||||||
|
SATURDAY: 6,
|
||||||
|
};
|
||||||
|
|
||||||
|
const MAX_RECURRING_WEEKS = 52;
|
||||||
|
|
||||||
|
function generateBookingCode(): string {
|
||||||
|
const alphabet = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
|
||||||
|
let code = '';
|
||||||
|
for (let i = 0; i < 6; i++) {
|
||||||
|
code += alphabet[randomInt(0, alphabet.length)];
|
||||||
|
}
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateRecurringGroup(
|
||||||
|
userId: string,
|
||||||
|
groupId: string,
|
||||||
|
input: UpdateRecurringGroupInput
|
||||||
|
): Promise<Result<RecurringBookingGroup>> {
|
||||||
|
const group = await db.recurringBookingGroup.findUnique({
|
||||||
|
where: { id: groupId },
|
||||||
|
include: {
|
||||||
|
complex: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
users: { where: { userId }, select: { userId: true } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!group) {
|
||||||
|
return err(Errors.notFound('Grupo de turnos fijos no encontrado.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (group.complex.users.length === 0) {
|
||||||
|
return err(Errors.forbidden('No tienes permisos para administrar este complejo.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (group.status === 'CANCELLED') {
|
||||||
|
return err(Errors.conflict('No se puede editar un grupo cancelado.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
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 typeof group.dayOfWeek },
|
||||||
|
orderBy: { startTime: 'asc' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!court) {
|
||||||
|
return err(Errors.notFound('La cancha seleccionada no existe en el complejo.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
return err(Errors.conflict('El horario seleccionado no esta disponible para esa cancha.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
const todayStart = new Date(
|
||||||
|
Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await db.$transaction(async (tx) => {
|
||||||
|
await tx.recurringBookingGroup.update({
|
||||||
|
where: { id: groupId },
|
||||||
|
data: {
|
||||||
|
courtId,
|
||||||
|
dayOfWeek: dayOfWeek as typeof group.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)}.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (txError) {
|
||||||
|
if (txError instanceof AdminBookingServiceError) {
|
||||||
|
return err(Errors.conflict(txError.message));
|
||||||
|
}
|
||||||
|
throw txError;
|
||||||
|
}
|
||||||
|
} 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) {
|
||||||
|
return err(Errors.conflict('Error al actualizar el grupo.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return ok({
|
||||||
|
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((b) => ({
|
||||||
|
id: b.id,
|
||||||
|
bookingCode: b.bookingCode,
|
||||||
|
complexId: b.court.complex.id,
|
||||||
|
complexName: b.court.complex.complexName,
|
||||||
|
courtId: b.court.id,
|
||||||
|
courtName: b.court.name,
|
||||||
|
sport: { id: b.court.sport.id, name: b.court.sport.name, slug: b.court.sport.slug },
|
||||||
|
date: formatIsoDate(b.bookingDate),
|
||||||
|
startTime: b.startTime,
|
||||||
|
endTime: b.endTime,
|
||||||
|
customerName: b.customerName,
|
||||||
|
customerPhone: b.customerPhone,
|
||||||
|
customerEmail: b.customerEmail,
|
||||||
|
price: 0,
|
||||||
|
status: b.status,
|
||||||
|
recurringGroupId: b.recurringGroupId,
|
||||||
|
createdAt: b.createdAt.toISOString(),
|
||||||
|
updatedAt: b.updatedAt.toISOString(),
|
||||||
|
})),
|
||||||
|
createdAt: updated.createdAt.toISOString(),
|
||||||
|
updatedAt: updated.updatedAt.toISOString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { handleResult } from '@/lib/http/handle-result';
|
||||||
|
import type { AppContext } from '@/types/hono';
|
||||||
|
import type { UpdateRecurringGroupInput } from '@repo/api-contract';
|
||||||
|
import { updateRecurringGroup } from './update-recurring-group.business';
|
||||||
|
|
||||||
|
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');
|
||||||
|
|
||||||
|
return handleResult(c, await updateRecurringGroup(user.id, groupId, payload));
|
||||||
|
}
|
||||||
@@ -1,597 +0,0 @@
|
|||||||
import { randomInt } from 'node:crypto';
|
|
||||||
import { CourtBookingStatus } from '@/generated/prisma/enums';
|
|
||||||
import { db } from '@/lib/prisma';
|
|
||||||
import { isSlotInPast } from '@/lib/slot-validator';
|
|
||||||
import { evaluatePlanUsage, parsePlanRules } from '@/modules/plan/services/plan-rules.service';
|
|
||||||
import type { DayOfWeek } from '@repo/api-contract';
|
|
||||||
import type {
|
|
||||||
AdminBooking,
|
|
||||||
CreateAdminBookingInput,
|
|
||||||
ListAdminBookingsQuery,
|
|
||||||
UpdateAdminBookingStatusInput,
|
|
||||||
} from '@repo/api-contract';
|
|
||||||
import { v7 as uuidv7 } from 'uuid';
|
|
||||||
|
|
||||||
type Slot = {
|
|
||||||
startTime: string;
|
|
||||||
endTime: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const DAY_OF_WEEK_BY_INDEX = [
|
|
||||||
'SUNDAY',
|
|
||||||
'MONDAY',
|
|
||||||
'TUESDAY',
|
|
||||||
'WEDNESDAY',
|
|
||||||
'THURSDAY',
|
|
||||||
'FRIDAY',
|
|
||||||
'SATURDAY',
|
|
||||||
] as const;
|
|
||||||
const BOOKING_CODE_ALPHABET = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
|
|
||||||
const BOOKING_CODE_LENGTH = 6;
|
|
||||||
|
|
||||||
export class AdminBookingServiceError extends Error {
|
|
||||||
status: 400 | 403 | 404 | 409;
|
|
||||||
|
|
||||||
constructor(message: string, status: 400 | 403 | 404 | 409) {
|
|
||||||
super(message);
|
|
||||||
this.name = 'AdminBookingServiceError';
|
|
||||||
this.status = status;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 bookingDate = new Date(Date.UTC(year, month - 1, day));
|
|
||||||
|
|
||||||
if (
|
|
||||||
Number.isNaN(bookingDate.getTime()) ||
|
|
||||||
bookingDate.getUTCFullYear() !== year ||
|
|
||||||
bookingDate.getUTCMonth() + 1 !== month ||
|
|
||||||
bookingDate.getUTCDate() !== day
|
|
||||||
) {
|
|
||||||
throw new AdminBookingServiceError('La fecha enviada no es valida.', 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
return bookingDate;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDayOfWeek(date: Date) {
|
|
||||||
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 formatIsoDate(date: Date): string {
|
|
||||||
return date.toISOString().slice(0, 10);
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
): Slot[] {
|
|
||||||
const slots: Slot[] = [];
|
|
||||||
|
|
||||||
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 resolvePrice(
|
|
||||||
court: {
|
|
||||||
basePrice: unknown;
|
|
||||||
priceRules: Array<{
|
|
||||||
dayOfWeek: string | null;
|
|
||||||
startTime: string | null;
|
|
||||||
endTime: string | null;
|
|
||||||
price: unknown;
|
|
||||||
}>;
|
|
||||||
},
|
|
||||||
dayOfWeek: string,
|
|
||||||
startTime: string,
|
|
||||||
endTime: string
|
|
||||||
): number {
|
|
||||||
const slotStart = toMinutes(startTime);
|
|
||||||
const slotEnd = toMinutes(endTime);
|
|
||||||
|
|
||||||
const matchingRules = court.priceRules
|
|
||||||
.filter((rule) => {
|
|
||||||
if (rule.dayOfWeek && rule.dayOfWeek !== dayOfWeek) return false;
|
|
||||||
if (!rule.startTime || !rule.endTime) return true;
|
|
||||||
return slotStart >= toMinutes(rule.startTime) && slotEnd <= toMinutes(rule.endTime);
|
|
||||||
})
|
|
||||||
.sort((first, second) => {
|
|
||||||
const firstSpecificity =
|
|
||||||
(first.dayOfWeek ? 2 : 0) + (first.startTime && first.endTime ? 1 : 0);
|
|
||||||
const secondSpecificity =
|
|
||||||
(second.dayOfWeek ? 2 : 0) + (second.startTime && second.endTime ? 1 : 0);
|
|
||||||
return secondSpecificity - firstSpecificity;
|
|
||||||
});
|
|
||||||
|
|
||||||
return Number(matchingRules[0]?.price ?? court.basePrice);
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapBookingResponse(booking: {
|
|
||||||
id: string;
|
|
||||||
bookingCode: string;
|
|
||||||
bookingDate: Date;
|
|
||||||
startTime: string;
|
|
||||||
endTime: string;
|
|
||||||
customerName: string;
|
|
||||||
customerPhone: string;
|
|
||||||
customerEmail: string;
|
|
||||||
status: 'CONFIRMED' | 'CANCELLED' | 'COMPLETED' | 'NOSHOW';
|
|
||||||
createdAt: Date;
|
|
||||||
updatedAt: Date;
|
|
||||||
court: {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
complex: {
|
|
||||||
id: string;
|
|
||||||
complexName: string;
|
|
||||||
};
|
|
||||||
sport: {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
slug: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
price?: number;
|
|
||||||
}): AdminBooking {
|
|
||||||
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,
|
|
||||||
createdAt: booking.createdAt.toISOString(),
|
|
||||||
updatedAt: booking.updatedAt.toISOString(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function listAdminBookings(
|
|
||||||
userId: string,
|
|
||||||
complexId: string,
|
|
||||||
query: ListAdminBookingsQuery
|
|
||||||
) {
|
|
||||||
await ensureComplexAccess(complexId, userId);
|
|
||||||
const fromDate = parseIsoDate(query.fromDate);
|
|
||||||
|
|
||||||
const bookings = await db.courtBooking.findMany({
|
|
||||||
where: {
|
|
||||||
bookingDate: {
|
|
||||||
gte: fromDate,
|
|
||||||
},
|
|
||||||
court: {
|
|
||||||
complexId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
court: {
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
name: true,
|
|
||||||
sport: {
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
name: true,
|
|
||||||
slug: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
complex: {
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
complexName: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
orderBy: [{ bookingDate: 'asc' }, { startTime: 'asc' }, { createdAt: 'asc' }],
|
|
||||||
});
|
|
||||||
|
|
||||||
const response = bookings.map((booking) => mapBookingResponse(booking));
|
|
||||||
|
|
||||||
return {
|
|
||||||
bookings: response,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createAdminBooking(
|
|
||||||
userId: string,
|
|
||||||
complexId: string,
|
|
||||||
input: CreateAdminBookingInput
|
|
||||||
) {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
const bookingDate = parseIsoDate(input.date);
|
|
||||||
const dayOfWeek = getDayOfWeek(bookingDate);
|
|
||||||
|
|
||||||
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 selectedStartMinutes = toMinutes(input.startTime);
|
|
||||||
const selectedEndMinutes = selectedStartMinutes + 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(bookingDate, input.startTime, court.slotDurationMinutes)) {
|
|
||||||
throw new AdminBookingServiceError('No se pueden crear reservas en el pasado.', 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let attempt = 0; attempt < 5; attempt += 1) {
|
|
||||||
try {
|
|
||||||
const booking = await db.$transaction(async (tx) => {
|
|
||||||
if (complex.plan) {
|
|
||||||
const rules = parsePlanRules(complex.plan.rules);
|
|
||||||
const bookingsForDate = await tx.courtBooking.count({
|
|
||||||
where: {
|
|
||||||
bookingDate,
|
|
||||||
status: 'CONFIRMED',
|
|
||||||
court: {
|
|
||||||
complexId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const courtsCount = await tx.court.count({
|
|
||||||
where: {
|
|
||||||
complexId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const violations = evaluatePlanUsage(rules, {
|
|
||||||
courtsCount,
|
|
||||||
bookingsToday: bookingsForDate,
|
|
||||||
});
|
|
||||||
|
|
||||||
const maxBookingsViolation = violations.find(
|
|
||||||
(violation) => violation.code === 'MAX_BOOKINGS_PER_DAY_REACHED'
|
|
||||||
);
|
|
||||||
|
|
||||||
if (maxBookingsViolation) {
|
|
||||||
throw new AdminBookingServiceError(maxBookingsViolation.message, 409);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const overlappingBooking = await tx.courtBooking.findFirst({
|
|
||||||
where: {
|
|
||||||
courtId: court.id,
|
|
||||||
bookingDate,
|
|
||||||
status: 'CONFIRMED',
|
|
||||||
startTime: {
|
|
||||||
lt: selectedSlot.endTime,
|
|
||||||
},
|
|
||||||
endTime: {
|
|
||||||
gt: selectedSlot.startTime,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (overlappingBooking) {
|
|
||||||
throw new AdminBookingServiceError('El horario seleccionado ya fue reservado.', 409);
|
|
||||||
}
|
|
||||||
|
|
||||||
return tx.courtBooking.create({
|
|
||||||
data: {
|
|
||||||
id: uuidv7(),
|
|
||||||
bookingCode: generateBookingCode(),
|
|
||||||
courtId: court.id,
|
|
||||||
bookingDate,
|
|
||||||
startTime: selectedSlot.startTime,
|
|
||||||
endTime: selectedSlot.endTime,
|
|
||||||
customerName: input.customerName.trim(),
|
|
||||||
customerPhone: input.customerPhone.trim(),
|
|
||||||
customerEmail: input.customerEmail?.trim() ?? '',
|
|
||||||
status: 'CONFIRMED',
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
court: {
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
name: true,
|
|
||||||
sport: {
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
name: true,
|
|
||||||
slug: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
complex: {
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
complexName: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const price = resolvePrice(court, dayOfWeek, selectedSlot.startTime, selectedSlot.endTime);
|
|
||||||
|
|
||||||
return mapBookingResponse({ ...booking, price });
|
|
||||||
} 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 updateAdminBookingStatus(
|
|
||||||
userId: string,
|
|
||||||
bookingId: string,
|
|
||||||
input: UpdateAdminBookingStatusInput
|
|
||||||
) {
|
|
||||||
const booking = await db.courtBooking.findFirst({
|
|
||||||
where: {
|
|
||||||
id: bookingId,
|
|
||||||
court: {
|
|
||||||
complex: {
|
|
||||||
users: {
|
|
||||||
some: {
|
|
||||||
userId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
court: {
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
name: 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 (input.status === 'CANCELLED' && booking.status !== 'CONFIRMED') {
|
|
||||||
throw new AdminBookingServiceError(
|
|
||||||
'Solo se pueden cancelar reservas en estado confirmada.',
|
|
||||||
409
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (input.status === 'COMPLETED' && booking.status !== 'CONFIRMED') {
|
|
||||||
throw new AdminBookingServiceError(
|
|
||||||
'Solo se pueden marcar como cumplidas las reservas confirmadas.',
|
|
||||||
409
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (input.status === 'NOSHOW' && booking.status !== 'CONFIRMED') {
|
|
||||||
throw new AdminBookingServiceError(
|
|
||||||
'Solo se pueden marcar como no show las reservas confirmadas.',
|
|
||||||
409
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await db.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: input.status,
|
|
||||||
changedAt: new Date(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (input.status === 'COMPLETED' || input.status === 'NOSHOW') {
|
|
||||||
await db.courtBooking.update({
|
|
||||||
where: { id: booking.id },
|
|
||||||
data: {
|
|
||||||
status: input.status,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// if the booking is cancelled we delete it to free up the slot, but we keep a log of it with the cancelled status
|
|
||||||
await db.courtBooking.delete({
|
|
||||||
where: { id: booking.id },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return mapBookingResponse({ ...booking, status: input.status });
|
|
||||||
}
|
|
||||||
6
apps/backend/src/modules/admin-booking/shared/errors.ts
Normal file
6
apps/backend/src/modules/admin-booking/shared/errors.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export class AdminBookingServiceError extends Error {
|
||||||
|
constructor(message: string) {
|
||||||
|
super(message);
|
||||||
|
this.name = 'AdminBookingServiceError';
|
||||||
|
}
|
||||||
|
}
|
||||||
216
apps/backend/src/modules/admin-booking/shared/helpers.ts
Normal file
216
apps/backend/src/modules/admin-booking/shared/helpers.ts
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
import { randomInt } from 'node:crypto';
|
||||||
|
import { Errors } from '@/lib/errors';
|
||||||
|
import { db } from '@/lib/prisma';
|
||||||
|
import type { Result } from '@/lib/result';
|
||||||
|
import { err, ok } from '@/lib/result';
|
||||||
|
import type { AdminBooking, DayOfWeek } from '@repo/api-contract';
|
||||||
|
|
||||||
|
export type Slot = { startTime: string; endTime: string };
|
||||||
|
|
||||||
|
const DAY_OF_WEEK_BY_INDEX = [
|
||||||
|
'SUNDAY',
|
||||||
|
'MONDAY',
|
||||||
|
'TUESDAY',
|
||||||
|
'WEDNESDAY',
|
||||||
|
'THURSDAY',
|
||||||
|
'FRIDAY',
|
||||||
|
'SATURDAY',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const BOOKING_CODE_ALPHABET = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
|
||||||
|
const BOOKING_CODE_LENGTH = 6;
|
||||||
|
|
||||||
|
export function toMinutes(value: string): number {
|
||||||
|
const [hours, minutes] = value.split(':').map((part) => Number(part));
|
||||||
|
return hours * 60 + minutes;
|
||||||
|
}
|
||||||
|
|
||||||
|
export 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')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseIsoDate(date: string): Result<Date> {
|
||||||
|
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(date);
|
||||||
|
|
||||||
|
if (!match) {
|
||||||
|
return err(Errors.validation('La fecha debe tener formato YYYY-MM-DD.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const year = Number(match[1]);
|
||||||
|
const month = Number(match[2]);
|
||||||
|
const day = Number(match[3]);
|
||||||
|
|
||||||
|
const bookingDate = new Date(Date.UTC(year, month - 1, day));
|
||||||
|
|
||||||
|
if (
|
||||||
|
Number.isNaN(bookingDate.getTime()) ||
|
||||||
|
bookingDate.getUTCFullYear() !== year ||
|
||||||
|
bookingDate.getUTCMonth() + 1 !== month ||
|
||||||
|
bookingDate.getUTCDate() !== day
|
||||||
|
) {
|
||||||
|
return err(Errors.validation('La fecha enviada no es valida.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return ok(bookingDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatIsoDate(date: Date): string {
|
||||||
|
return date.toISOString().slice(0, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDayOfWeek(date: Date): Result<DayOfWeek> {
|
||||||
|
const dayOfWeek = DAY_OF_WEEK_BY_INDEX[date.getUTCDay()];
|
||||||
|
|
||||||
|
if (!dayOfWeek) {
|
||||||
|
return err(Errors.validation('No se pudo resolver el dia de la semana.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return ok(dayOfWeek);
|
||||||
|
}
|
||||||
|
|
||||||
|
export 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildSlots(
|
||||||
|
availability: Array<{ startTime: string; endTime: string }>,
|
||||||
|
slotDurationMinutes: number
|
||||||
|
): Slot[] {
|
||||||
|
const slots: Slot[] = [];
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function ensureComplexAccess(
|
||||||
|
complexId: string,
|
||||||
|
userId: string
|
||||||
|
): Promise<
|
||||||
|
Result<{
|
||||||
|
id: string;
|
||||||
|
complexName: string;
|
||||||
|
plan: { rules: string } | null;
|
||||||
|
}>
|
||||||
|
> {
|
||||||
|
const complexUser = await db.complexUser.findUnique({
|
||||||
|
where: {
|
||||||
|
complexId_userId: { complexId, userId },
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
complex: {
|
||||||
|
select: { id: true, complexName: true, plan: { select: { rules: true } } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!complexUser) {
|
||||||
|
return err(Errors.forbidden('No tienes permisos para administrar este complejo.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return ok(complexUser.complex);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolvePrice(
|
||||||
|
court: {
|
||||||
|
basePrice: unknown;
|
||||||
|
priceRules: Array<{
|
||||||
|
dayOfWeek: string | null;
|
||||||
|
startTime: string | null;
|
||||||
|
endTime: string | null;
|
||||||
|
price: unknown;
|
||||||
|
}>;
|
||||||
|
},
|
||||||
|
dayOfWeek: string,
|
||||||
|
startTime: string,
|
||||||
|
endTime: string
|
||||||
|
): number {
|
||||||
|
const slotStart = toMinutes(startTime);
|
||||||
|
const slotEnd = toMinutes(endTime);
|
||||||
|
|
||||||
|
const matchingRules = court.priceRules
|
||||||
|
.filter((rule) => {
|
||||||
|
if (rule.dayOfWeek && rule.dayOfWeek !== dayOfWeek) return false;
|
||||||
|
if (!rule.startTime || !rule.endTime) return true;
|
||||||
|
return slotStart >= toMinutes(rule.startTime) && slotEnd <= toMinutes(rule.endTime);
|
||||||
|
})
|
||||||
|
.sort((first, second) => {
|
||||||
|
const firstSpecificity =
|
||||||
|
(first.dayOfWeek ? 2 : 0) + (first.startTime && first.endTime ? 1 : 0);
|
||||||
|
const secondSpecificity =
|
||||||
|
(second.dayOfWeek ? 2 : 0) + (second.startTime && second.endTime ? 1 : 0);
|
||||||
|
return secondSpecificity - firstSpecificity;
|
||||||
|
});
|
||||||
|
|
||||||
|
return Number(matchingRules[0]?.price ?? court.basePrice);
|
||||||
|
}
|
||||||
|
|
||||||
|
export 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;
|
||||||
|
}): AdminBooking {
|
||||||
|
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(),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,17 +1,17 @@
|
|||||||
import { validate } from '@/lib/http/validate';
|
import { validate } from '@/lib/http/validate';
|
||||||
import { requireAuth } from '@/middlewares/require-auth.middleware';
|
import { requireAuth } from '@/middlewares/require-auth.middleware';
|
||||||
import { requireSuperAdmin } from '@/middlewares/require-super-admin.middleware';
|
import { requireSuperAdmin } from '@/middlewares/require-super-admin.middleware';
|
||||||
import { blockUserHandler } from '@/modules/admin/handlers/block-user.handler';
|
import { blockUserHandler } from '@/modules/admin/features/block-user/block-user.handler';
|
||||||
import { createPlanHandler } from '@/modules/admin/handlers/create-plan.handler';
|
import { createPlanHandler } from '@/modules/admin/features/create-plan/create-plan.handler';
|
||||||
import { deletePlanHandler } from '@/modules/admin/handlers/delete-plan.handler';
|
import { deletePlanHandler } from '@/modules/admin/features/delete-plan/delete-plan.handler';
|
||||||
import { getGeoStatsHandler } from '@/modules/admin/handlers/get-geo-stats.handler';
|
import { getGeoStatsHandler } from '@/modules/admin/features/get-geo-stats/get-geo-stats.handler';
|
||||||
import { getUserSessionsHandler } from '@/modules/admin/handlers/get-user-sessions.handler';
|
import { getUserSessionsHandler } from '@/modules/admin/features/get-user-sessions/get-user-sessions.handler';
|
||||||
import { listComplexesHandler } from '@/modules/admin/handlers/list-complexes.handler';
|
import { listComplexesHandler } from '@/modules/admin/features/list-complexes/list-complexes.handler';
|
||||||
import { listPlansAdminHandler } from '@/modules/admin/handlers/list-plans-admin.handler';
|
import { listPlansAdminHandler } from '@/modules/admin/features/list-plans-admin/list-plans-admin.handler';
|
||||||
import { listUsersHandler } from '@/modules/admin/handlers/list-users.handler';
|
import { listUsersHandler } from '@/modules/admin/features/list-users/list-users.handler';
|
||||||
import { revokeAllSessionsHandler } from '@/modules/admin/handlers/revoke-all-sessions.handler';
|
import { revokeAllSessionsHandler } from '@/modules/admin/features/revoke-all-sessions/revoke-all-sessions.handler';
|
||||||
import { unblockUserHandler } from '@/modules/admin/handlers/unblock-user.handler';
|
import { unblockUserHandler } from '@/modules/admin/features/unblock-user/unblock-user.handler';
|
||||||
import { updatePlanHandler } from '@/modules/admin/handlers/update-plan.handler';
|
import { updatePlanHandler } from '@/modules/admin/features/update-plan/update-plan.handler';
|
||||||
import type { AppEnv } from '@/types/hono';
|
import type { AppEnv } from '@/types/hono';
|
||||||
import {
|
import {
|
||||||
adminBlockUserSchema,
|
adminBlockUserSchema,
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { db } from '@/lib/prisma';
|
||||||
|
import { AdminServiceError } from '../../shared/errors';
|
||||||
|
|
||||||
|
export async function blockUser(userId: string, banReason?: string): Promise<void> {
|
||||||
|
const target = await db.user.findUnique({
|
||||||
|
where: { id: userId },
|
||||||
|
select: { role: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!target) {
|
||||||
|
throw new AdminServiceError('Usuario no encontrado.', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target.role === 'super_admin') {
|
||||||
|
throw new AdminServiceError('No se puede bloquear un super_admin.', 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.user.update({
|
||||||
|
where: { id: userId },
|
||||||
|
data: {
|
||||||
|
banned: true,
|
||||||
|
bannedAt: new Date(),
|
||||||
|
banReason: banReason ?? null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { AdminServiceError, blockUser } from '@/modules/admin/services/admin-users.service';
|
|
||||||
import type { AppContext } from '@/types/hono';
|
import type { AppContext } from '@/types/hono';
|
||||||
import type { AdminBlockUserInput } from '@repo/api-contract';
|
import type { AdminBlockUserInput } from '@repo/api-contract';
|
||||||
|
import { AdminServiceError } from '../../shared/errors';
|
||||||
|
import { blockUser } from './block-user.business';
|
||||||
|
|
||||||
type BlockUserParams = { id: string };
|
type BlockUserParams = { id: string };
|
||||||
|
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { db } from '@/lib/prisma';
|
||||||
|
import type { AdminCreatePlanInput } from '@repo/api-contract';
|
||||||
|
import { v7 as uuidv7 } from 'uuid';
|
||||||
|
|
||||||
|
export async function createPlan(input: AdminCreatePlanInput) {
|
||||||
|
const existing = await db.plan.findUnique({
|
||||||
|
where: { code: input.code },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
return { ok: false as const, message: 'Ya existe un plan con ese código.' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const plan = await db.plan.create({
|
||||||
|
data: {
|
||||||
|
code: input.code,
|
||||||
|
name: input.name,
|
||||||
|
price: input.price,
|
||||||
|
rules: input.rules,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: true as const,
|
||||||
|
data: {
|
||||||
|
code: plan.code,
|
||||||
|
name: plan.name,
|
||||||
|
price: Number(plan.price),
|
||||||
|
rules: plan.rules,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import type { AppContext } from '@/types/hono';
|
||||||
|
import type { AdminCreatePlanInput } from '@repo/api-contract';
|
||||||
|
import { createPlan } from './create-plan.business';
|
||||||
|
|
||||||
|
export async function createPlanHandler(c: AppContext) {
|
||||||
|
const body = c.req.valid('json' as never) as AdminCreatePlanInput;
|
||||||
|
|
||||||
|
const result = await createPlan(body);
|
||||||
|
|
||||||
|
if (!result.ok) {
|
||||||
|
return c.json({ message: result.message }, 409);
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.json(result.data, 201);
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { db } from '@/lib/prisma';
|
||||||
|
|
||||||
|
export async function deletePlan(code: string) {
|
||||||
|
const existing = await db.plan.findUnique({
|
||||||
|
where: { code },
|
||||||
|
include: {
|
||||||
|
_count: { select: { complexes: true } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!existing) {
|
||||||
|
return { ok: false as const, message: 'Plan no encontrado.' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existing._count.complexes > 0) {
|
||||||
|
return {
|
||||||
|
ok: false as const,
|
||||||
|
message: 'No se puede eliminar un plan que tiene complejos asignados.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.plan.delete({ where: { code } });
|
||||||
|
|
||||||
|
return { ok: true as const, message: 'Plan eliminado correctamente.' };
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import type { AppContext } from '@/types/hono';
|
||||||
|
import { deletePlan } from './delete-plan.business';
|
||||||
|
|
||||||
|
type DeletePlanParams = { code: string };
|
||||||
|
|
||||||
|
export async function deletePlanHandler(c: AppContext) {
|
||||||
|
const { code } = c.req.valid('param' as never) as DeletePlanParams;
|
||||||
|
|
||||||
|
const result = await deletePlan(code);
|
||||||
|
|
||||||
|
if (!result.ok) {
|
||||||
|
const status = result.message === 'Plan no encontrado.' ? 404 : 409;
|
||||||
|
return c.json({ message: result.message }, status);
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.json({ message: result.message });
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { getGeoStats } from '@/modules/admin/services/admin-geo.service';
|
|
||||||
import type { AppEnv } from '@/types/hono';
|
import type { AppEnv } from '@/types/hono';
|
||||||
import type { Handler } from 'hono';
|
import type { Handler } from 'hono';
|
||||||
|
import { getGeoStats } from './get-geo-stats.business';
|
||||||
|
|
||||||
export const getGeoStatsHandler: Handler<AppEnv> = async (c) => {
|
export const getGeoStatsHandler: Handler<AppEnv> = async (c) => {
|
||||||
const stats = await getGeoStats();
|
const stats = await getGeoStats();
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { fetchGeoInfo } from '@/lib/geoip';
|
||||||
|
import { db } from '@/lib/prisma';
|
||||||
|
|
||||||
|
type AdminUserSession = {
|
||||||
|
id: string;
|
||||||
|
createdAt: string;
|
||||||
|
expiresAt: string;
|
||||||
|
ipAddress: string | null;
|
||||||
|
userAgent: string | null;
|
||||||
|
city: string | null;
|
||||||
|
country: string | null;
|
||||||
|
countryCode: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function getUserSessions(userId: string): Promise<AdminUserSession[]> {
|
||||||
|
const sessions = await db.session.findMany({
|
||||||
|
where: { userId },
|
||||||
|
orderBy: { createdAt: 'desc' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const uniqueIps = [...new Set(sessions.map((s) => s.ipAddress).filter(Boolean))] as string[];
|
||||||
|
const geoResults = await Promise.all(uniqueIps.map((ip) => fetchGeoInfo(ip)));
|
||||||
|
const geoMap = new Map<
|
||||||
|
string,
|
||||||
|
{ city: string | null; country: string | null; countryCode: string | null }
|
||||||
|
>();
|
||||||
|
for (let i = 0; i < uniqueIps.length; i++) {
|
||||||
|
const info = geoResults[i];
|
||||||
|
geoMap.set(uniqueIps[i], {
|
||||||
|
city: info?.city ?? null,
|
||||||
|
country: info?.country ?? null,
|
||||||
|
countryCode: info?.countryCode ?? null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return sessions.map((s) => {
|
||||||
|
const geo = s.ipAddress ? geoMap.get(s.ipAddress) : null;
|
||||||
|
return {
|
||||||
|
id: s.id,
|
||||||
|
createdAt: s.createdAt.toISOString(),
|
||||||
|
expiresAt: s.expiresAt.toISOString(),
|
||||||
|
ipAddress: s.ipAddress,
|
||||||
|
userAgent: s.userAgent,
|
||||||
|
city: geo?.city ?? null,
|
||||||
|
country: geo?.country ?? null,
|
||||||
|
countryCode: geo?.countryCode ?? null,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { getUserSessions } from '@/modules/admin/services/admin-users.service';
|
|
||||||
import type { AppContext } from '@/types/hono';
|
import type { AppContext } from '@/types/hono';
|
||||||
|
import { getUserSessions } from './get-user-sessions.business';
|
||||||
|
|
||||||
type UserSessionsParams = { id: string };
|
type UserSessionsParams = { id: string };
|
||||||
|
|
||||||
@@ -26,9 +26,7 @@ export async function getComplexStatsList(): Promise<ComplexStats[]> {
|
|||||||
plan: true,
|
plan: true,
|
||||||
users: true,
|
users: true,
|
||||||
courts: {
|
courts: {
|
||||||
include: {
|
include: { bookings: true },
|
||||||
bookings: true,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { getComplexStatsList } from '@/modules/admin/services/complex-stats.service';
|
|
||||||
import type { AppContext } from '@/types/hono';
|
import type { AppContext } from '@/types/hono';
|
||||||
|
import { getComplexStatsList } from './list-complexes.business';
|
||||||
|
|
||||||
export async function listComplexesHandler(c: AppContext) {
|
export async function listComplexesHandler(c: AppContext) {
|
||||||
const stats = await getComplexStatsList();
|
const stats = await getComplexStatsList();
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { db } from '@/lib/prisma';
|
||||||
|
|
||||||
|
export async function listPlansAdmin() {
|
||||||
|
const plans = await db.plan.findMany({
|
||||||
|
orderBy: { price: 'asc' },
|
||||||
|
include: {
|
||||||
|
_count: { select: { complexes: true } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return plans.map((plan) => ({
|
||||||
|
code: plan.code,
|
||||||
|
name: plan.name,
|
||||||
|
price: Number(plan.price),
|
||||||
|
rules: plan.rules,
|
||||||
|
lastUpdatedAt: plan.lastUpdatedAt.toISOString(),
|
||||||
|
complexCount: plan._count.complexes,
|
||||||
|
}));
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import type { AppContext } from '@/types/hono';
|
||||||
|
import { listPlansAdmin } from './list-plans-admin.business';
|
||||||
|
|
||||||
|
export async function listPlansAdminHandler(c: AppContext) {
|
||||||
|
const plans = await listPlansAdmin();
|
||||||
|
return c.json(plans);
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { db } from '@/lib/prisma';
|
||||||
|
|
||||||
|
type AdminUser = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
role: string;
|
||||||
|
banned: boolean;
|
||||||
|
bannedAt: string | null;
|
||||||
|
banReason: string | null;
|
||||||
|
createdAt: string;
|
||||||
|
complexCount: number;
|
||||||
|
activeSessions: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function listAdminUsers(search?: string): Promise<AdminUser[]> {
|
||||||
|
const users = await db.user.findMany({
|
||||||
|
where: search
|
||||||
|
? {
|
||||||
|
OR: [
|
||||||
|
{ name: { contains: search, mode: 'insensitive' } },
|
||||||
|
{ email: { contains: search, mode: 'insensitive' } },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
include: {
|
||||||
|
_count: { select: { complexes: true, sessions: true } },
|
||||||
|
},
|
||||||
|
orderBy: { createdAt: 'desc' },
|
||||||
|
});
|
||||||
|
|
||||||
|
return users.map((user) => ({
|
||||||
|
id: user.id,
|
||||||
|
name: user.name,
|
||||||
|
email: user.email,
|
||||||
|
role: user.role,
|
||||||
|
banned: user.banned ?? false,
|
||||||
|
bannedAt: user.bannedAt?.toISOString() ?? null,
|
||||||
|
banReason: user.banReason ?? null,
|
||||||
|
createdAt: user.createdAt.toISOString(),
|
||||||
|
complexCount: user._count.complexes,
|
||||||
|
activeSessions: user._count.sessions,
|
||||||
|
}));
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { listAdminUsers } from '@/modules/admin/services/admin-users.service';
|
|
||||||
import type { AppContext } from '@/types/hono';
|
import type { AppContext } from '@/types/hono';
|
||||||
|
import { listAdminUsers } from './list-users.business';
|
||||||
|
|
||||||
export async function listUsersHandler(c: AppContext) {
|
export async function listUsersHandler(c: AppContext) {
|
||||||
const search = c.req.query('search');
|
const search = c.req.query('search');
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { db } from '@/lib/prisma';
|
||||||
|
|
||||||
|
export async function revokeAllUserSessions(userId: string): Promise<number> {
|
||||||
|
const result = await db.session.deleteMany({
|
||||||
|
where: { userId },
|
||||||
|
});
|
||||||
|
|
||||||
|
return result.count;
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { revokeAllUserSessions } from '@/modules/admin/services/admin-users.service';
|
|
||||||
import type { AppContext } from '@/types/hono';
|
import type { AppContext } from '@/types/hono';
|
||||||
|
import { revokeAllUserSessions } from './revoke-all-sessions.business';
|
||||||
|
|
||||||
type RevokeSessionsParams = { id: string };
|
type RevokeSessionsParams = { id: string };
|
||||||
|
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { db } from '@/lib/prisma';
|
||||||
|
|
||||||
|
export async function unblockUser(userId: string): Promise<void> {
|
||||||
|
await db.user.update({
|
||||||
|
where: { id: userId },
|
||||||
|
data: {
|
||||||
|
banned: false,
|
||||||
|
bannedAt: null,
|
||||||
|
banReason: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { unblockUser } from '@/modules/admin/services/admin-users.service';
|
|
||||||
import type { AppContext } from '@/types/hono';
|
import type { AppContext } from '@/types/hono';
|
||||||
|
import { unblockUser } from './unblock-user.business';
|
||||||
|
|
||||||
type UnblockUserParams = { id: string };
|
type UnblockUserParams = { id: string };
|
||||||
|
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { db } from '@/lib/prisma';
|
||||||
|
import type { AdminUpdatePlanInput } from '@repo/api-contract';
|
||||||
|
|
||||||
|
export async function updatePlan(code: string, input: AdminUpdatePlanInput) {
|
||||||
|
const existing = await db.plan.findUnique({ where: { code } });
|
||||||
|
|
||||||
|
if (!existing) {
|
||||||
|
return { ok: false as const, message: 'Plan no encontrado.' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const plan = await db.plan.update({
|
||||||
|
where: { code },
|
||||||
|
data: {
|
||||||
|
...(input.name !== undefined && { name: input.name }),
|
||||||
|
...(input.price !== undefined && { price: input.price }),
|
||||||
|
...(input.rules !== undefined && { rules: input.rules }),
|
||||||
|
lastUpdatedAt: new Date(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: true as const,
|
||||||
|
data: {
|
||||||
|
code: plan.code,
|
||||||
|
name: plan.name,
|
||||||
|
price: Number(plan.price),
|
||||||
|
rules: plan.rules,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import type { AppContext } from '@/types/hono';
|
||||||
|
import type { AdminUpdatePlanInput } from '@repo/api-contract';
|
||||||
|
import { updatePlan } from './update-plan.business';
|
||||||
|
|
||||||
|
type UpdatePlanParams = { code: string };
|
||||||
|
|
||||||
|
export async function updatePlanHandler(c: AppContext) {
|
||||||
|
const { code } = c.req.valid('param' as never) as UpdatePlanParams;
|
||||||
|
const body = c.req.valid('json' as never) as AdminUpdatePlanInput;
|
||||||
|
|
||||||
|
const result = await updatePlan(code, body);
|
||||||
|
|
||||||
|
if (!result.ok) {
|
||||||
|
return c.json({ message: result.message }, 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.json(result.data);
|
||||||
|
}
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import { db } from '@/lib/prisma';
|
|
||||||
import type { AppContext } from '@/types/hono';
|
|
||||||
import type { AdminCreatePlanInput } from '@repo/api-contract';
|
|
||||||
|
|
||||||
export async function createPlanHandler(c: AppContext) {
|
|
||||||
const body = c.req.valid('json' as never) as AdminCreatePlanInput;
|
|
||||||
|
|
||||||
const existing = await db.plan.findUnique({
|
|
||||||
where: { code: body.code },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (existing) {
|
|
||||||
return c.json({ message: 'Ya existe un plan con ese código.' }, 409);
|
|
||||||
}
|
|
||||||
|
|
||||||
const plan = await db.plan.create({
|
|
||||||
data: {
|
|
||||||
code: body.code,
|
|
||||||
name: body.name,
|
|
||||||
price: body.price,
|
|
||||||
rules: body.rules,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return c.json(
|
|
||||||
{
|
|
||||||
code: plan.code,
|
|
||||||
name: plan.name,
|
|
||||||
price: Number(plan.price),
|
|
||||||
rules: plan.rules,
|
|
||||||
},
|
|
||||||
201
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
import { db } from '@/lib/prisma';
|
|
||||||
import type { AppContext } from '@/types/hono';
|
|
||||||
|
|
||||||
type DeletePlanParams = { code: string };
|
|
||||||
|
|
||||||
export async function deletePlanHandler(c: AppContext) {
|
|
||||||
const { code } = c.req.valid('param' as never) as DeletePlanParams;
|
|
||||||
|
|
||||||
const existing = await db.plan.findUnique({
|
|
||||||
where: { code },
|
|
||||||
include: {
|
|
||||||
_count: {
|
|
||||||
select: { complexes: true },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!existing) {
|
|
||||||
return c.json({ message: 'Plan no encontrado.' }, 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (existing._count.complexes > 0) {
|
|
||||||
return c.json({ message: 'No se puede eliminar un plan que tiene complejos asignados.' }, 409);
|
|
||||||
}
|
|
||||||
|
|
||||||
await db.plan.delete({
|
|
||||||
where: { code },
|
|
||||||
});
|
|
||||||
|
|
||||||
return c.json({ message: 'Plan eliminado correctamente.' });
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
import { db } from '@/lib/prisma';
|
|
||||||
import type { AppContext } from '@/types/hono';
|
|
||||||
|
|
||||||
export async function listPlansAdminHandler(c: AppContext) {
|
|
||||||
const plans = await db.plan.findMany({
|
|
||||||
orderBy: { price: 'asc' },
|
|
||||||
include: {
|
|
||||||
_count: {
|
|
||||||
select: { complexes: true },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return c.json(
|
|
||||||
plans.map((plan) => ({
|
|
||||||
code: plan.code,
|
|
||||||
name: plan.name,
|
|
||||||
price: Number(plan.price),
|
|
||||||
rules: plan.rules,
|
|
||||||
lastUpdatedAt: plan.lastUpdatedAt.toISOString(),
|
|
||||||
complexCount: plan._count.complexes,
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import { db } from '@/lib/prisma';
|
|
||||||
import type { AppContext } from '@/types/hono';
|
|
||||||
import type { AdminUpdatePlanInput } from '@repo/api-contract';
|
|
||||||
|
|
||||||
type UpdatePlanParams = { code: string };
|
|
||||||
|
|
||||||
export async function updatePlanHandler(c: AppContext) {
|
|
||||||
const { code } = c.req.valid('param' as never) as UpdatePlanParams;
|
|
||||||
const body = c.req.valid('json' as never) as AdminUpdatePlanInput;
|
|
||||||
|
|
||||||
const existing = await db.plan.findUnique({
|
|
||||||
where: { code },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!existing) {
|
|
||||||
return c.json({ message: 'Plan no encontrado.' }, 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
const plan = await db.plan.update({
|
|
||||||
where: { code },
|
|
||||||
data: {
|
|
||||||
...(body.name !== undefined && { name: body.name }),
|
|
||||||
...(body.price !== undefined && { price: body.price }),
|
|
||||||
...(body.rules !== undefined && { rules: body.rules }),
|
|
||||||
lastUpdatedAt: new Date(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return c.json({
|
|
||||||
code: plan.code,
|
|
||||||
name: plan.name,
|
|
||||||
price: Number(plan.price),
|
|
||||||
rules: plan.rules,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,146 +0,0 @@
|
|||||||
import { fetchGeoInfo } from '@/lib/geoip';
|
|
||||||
import { db } from '@/lib/prisma';
|
|
||||||
|
|
||||||
type AdminUser = {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
email: string;
|
|
||||||
role: string;
|
|
||||||
banned: boolean;
|
|
||||||
bannedAt: string | null;
|
|
||||||
banReason: string | null;
|
|
||||||
createdAt: string;
|
|
||||||
complexCount: number;
|
|
||||||
activeSessions: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function listAdminUsers(search?: string): Promise<AdminUser[]> {
|
|
||||||
const users = await db.user.findMany({
|
|
||||||
where: search
|
|
||||||
? {
|
|
||||||
OR: [
|
|
||||||
{ name: { contains: search, mode: 'insensitive' } },
|
|
||||||
{ email: { contains: search, mode: 'insensitive' } },
|
|
||||||
],
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
include: {
|
|
||||||
_count: {
|
|
||||||
select: { complexes: true, sessions: true },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
orderBy: { createdAt: 'desc' },
|
|
||||||
});
|
|
||||||
|
|
||||||
return users.map((user) => ({
|
|
||||||
id: user.id,
|
|
||||||
name: user.name,
|
|
||||||
email: user.email,
|
|
||||||
role: user.role,
|
|
||||||
banned: user.banned ?? false,
|
|
||||||
bannedAt: user.bannedAt?.toISOString() ?? null,
|
|
||||||
banReason: user.banReason ?? null,
|
|
||||||
createdAt: user.createdAt.toISOString(),
|
|
||||||
complexCount: user._count.complexes,
|
|
||||||
activeSessions: user._count.sessions,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
type AdminUserSession = {
|
|
||||||
id: string;
|
|
||||||
createdAt: string;
|
|
||||||
expiresAt: string;
|
|
||||||
ipAddress: string | null;
|
|
||||||
userAgent: string | null;
|
|
||||||
city: string | null;
|
|
||||||
country: string | null;
|
|
||||||
countryCode: string | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function getUserSessions(userId: string): Promise<AdminUserSession[]> {
|
|
||||||
const sessions = await db.session.findMany({
|
|
||||||
where: { userId },
|
|
||||||
orderBy: { createdAt: 'desc' },
|
|
||||||
});
|
|
||||||
|
|
||||||
const uniqueIps = [...new Set(sessions.map((s) => s.ipAddress).filter(Boolean))] as string[];
|
|
||||||
const geoResults = await Promise.all(uniqueIps.map((ip) => fetchGeoInfo(ip)));
|
|
||||||
const geoMap = new Map<
|
|
||||||
string,
|
|
||||||
{ city: string | null; country: string | null; countryCode: string | null }
|
|
||||||
>();
|
|
||||||
for (let i = 0; i < uniqueIps.length; i++) {
|
|
||||||
const info = geoResults[i];
|
|
||||||
geoMap.set(uniqueIps[i], {
|
|
||||||
city: info?.city ?? null,
|
|
||||||
country: info?.country ?? null,
|
|
||||||
countryCode: info?.countryCode ?? null,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return sessions.map((s) => {
|
|
||||||
const geo = s.ipAddress ? geoMap.get(s.ipAddress) : null;
|
|
||||||
return {
|
|
||||||
id: s.id,
|
|
||||||
createdAt: s.createdAt.toISOString(),
|
|
||||||
expiresAt: s.expiresAt.toISOString(),
|
|
||||||
ipAddress: s.ipAddress,
|
|
||||||
userAgent: s.userAgent,
|
|
||||||
city: geo?.city ?? null,
|
|
||||||
country: geo?.country ?? null,
|
|
||||||
countryCode: geo?.countryCode ?? null,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export class AdminServiceError extends Error {
|
|
||||||
status: 400 | 403 | 404;
|
|
||||||
constructor(message: string, status: 400 | 403 | 404 = 400) {
|
|
||||||
super(message);
|
|
||||||
this.status = status;
|
|
||||||
this.name = 'AdminServiceError';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function blockUser(userId: string, banReason?: string): Promise<void> {
|
|
||||||
const target = await db.user.findUnique({
|
|
||||||
where: { id: userId },
|
|
||||||
select: { role: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!target) {
|
|
||||||
throw new AdminServiceError('Usuario no encontrado.', 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (target.role === 'super_admin') {
|
|
||||||
throw new AdminServiceError('No se puede bloquear un super_admin.', 403);
|
|
||||||
}
|
|
||||||
|
|
||||||
await db.user.update({
|
|
||||||
where: { id: userId },
|
|
||||||
data: {
|
|
||||||
banned: true,
|
|
||||||
bannedAt: new Date(),
|
|
||||||
banReason: banReason ?? null,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function unblockUser(userId: string): Promise<void> {
|
|
||||||
await db.user.update({
|
|
||||||
where: { id: userId },
|
|
||||||
data: {
|
|
||||||
banned: false,
|
|
||||||
bannedAt: null,
|
|
||||||
banReason: null,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function revokeAllUserSessions(userId: string): Promise<number> {
|
|
||||||
const result = await db.session.deleteMany({
|
|
||||||
where: { userId },
|
|
||||||
});
|
|
||||||
|
|
||||||
return result.count;
|
|
||||||
}
|
|
||||||
9
apps/backend/src/modules/admin/shared/errors.ts
Normal file
9
apps/backend/src/modules/admin/shared/errors.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
export class AdminServiceError extends Error {
|
||||||
|
status: 400 | 403 | 404;
|
||||||
|
|
||||||
|
constructor(message: string, status: 400 | 403 | 404 = 400) {
|
||||||
|
super(message);
|
||||||
|
this.status = status;
|
||||||
|
this.name = 'AdminServiceError';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,17 +1,17 @@
|
|||||||
import { requireAuth } from '@/middlewares/require-auth.middleware';
|
import { requireAuth } from '@/middlewares/require-auth.middleware';
|
||||||
import { acceptComplexInvitationsHandler } from '@/modules/complex/handlers/accept-complex-invitations.handler';
|
import { acceptComplexInvitationsHandler } from '@/modules/complex/features/accept-complex-invitations/accept-complex-invitations.handler';
|
||||||
import { cancelComplexInvitationHandler } from '@/modules/complex/handlers/cancel-complex-invitation.handler';
|
import { cancelComplexInvitationHandler } from '@/modules/complex/features/cancel-complex-invitation/cancel-complex-invitation.handler';
|
||||||
import { createComplexHandler } from '@/modules/complex/handlers/create-complex.handler';
|
import { createComplexHandler } from '@/modules/complex/features/create-complex/create-complex.handler';
|
||||||
import { getComplexByIdHandler } from '@/modules/complex/handlers/get-complex-by-id.handler';
|
import { getComplexByIdHandler } from '@/modules/complex/features/get-complex-by-id/get-complex-by-id.handler';
|
||||||
import { getComplexBySlugHandler } from '@/modules/complex/handlers/get-complex-by-slug.handler';
|
import { getComplexBySlugHandler } from '@/modules/complex/features/get-complex-by-slug/get-complex-by-slug.handler';
|
||||||
import { getCurrentComplexHandler } from '@/modules/complex/handlers/get-current-complex.handler';
|
import { getCurrentComplexHandler } from '@/modules/complex/features/get-current-complex/get-current-complex.handler';
|
||||||
import { inviteComplexUserHandler } from '@/modules/complex/handlers/invite-complex-user.handler';
|
import { inviteComplexUserHandler } from '@/modules/complex/features/invite-complex-user/invite-complex-user.handler';
|
||||||
import { listComplexUsersHandler } from '@/modules/complex/handlers/list-complex-users.handler';
|
import { listComplexUsersHandler } from '@/modules/complex/features/list-complex-users/list-complex-users.handler';
|
||||||
import { listMyComplexesHandler } from '@/modules/complex/handlers/list-my-complexes.handler';
|
import { listMyComplexesHandler } from '@/modules/complex/features/list-my-complexes/list-my-complexes.handler';
|
||||||
import { resendComplexInvitationHandler } from '@/modules/complex/handlers/resend-complex-invitation.handler';
|
import { resendComplexInvitationHandler } from '@/modules/complex/features/resend-complex-invitation/resend-complex-invitation.handler';
|
||||||
import { revokeComplexUserHandler } from '@/modules/complex/handlers/revoke-complex-user.handler';
|
import { revokeComplexUserHandler } from '@/modules/complex/features/revoke-complex-user/revoke-complex-user.handler';
|
||||||
import { selectComplexHandler } from '@/modules/complex/handlers/select-complex.handler';
|
import { selectComplexHandler } from '@/modules/complex/features/select-complex/select-complex.handler';
|
||||||
import { updateComplexHandler } from '@/modules/complex/handlers/update-complex.handler';
|
import { updateComplexHandler } from '@/modules/complex/features/update-complex/update-complex.handler';
|
||||||
import type { AppEnv } from '@/types/hono';
|
import type { AppEnv } from '@/types/hono';
|
||||||
import { zValidator } from '@hono/zod-validator';
|
import { zValidator } from '@hono/zod-validator';
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import { Errors } from '@/lib/errors';
|
||||||
|
import { db } from '@/lib/prisma';
|
||||||
|
import { type Result, err, ok } from '@/lib/result';
|
||||||
|
import type {
|
||||||
|
AcceptComplexInvitationsInput,
|
||||||
|
AcceptComplexInvitationsResponse,
|
||||||
|
} from '@repo/api-contract';
|
||||||
|
import { hashToken, normalizeEmail } from '../../shared/members-helpers';
|
||||||
|
|
||||||
|
export async function acceptComplexInvitations(
|
||||||
|
userId: string,
|
||||||
|
email: string,
|
||||||
|
input: AcceptComplexInvitationsInput = {}
|
||||||
|
): Promise<Result<AcceptComplexInvitationsResponse>> {
|
||||||
|
const normalizedEmail = normalizeEmail(email);
|
||||||
|
const tokenHash = input.inviteToken ? hashToken(input.inviteToken) : null;
|
||||||
|
|
||||||
|
const invitations = await db.complexInvitation.findMany({
|
||||||
|
where: {
|
||||||
|
email: normalizedEmail,
|
||||||
|
acceptedAt: null,
|
||||||
|
revokedAt: null,
|
||||||
|
expiresAt: { gte: new Date() },
|
||||||
|
...(tokenHash ? { tokenHash } : {}),
|
||||||
|
},
|
||||||
|
orderBy: { createdAt: 'asc' },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (tokenHash && invitations.length === 0) {
|
||||||
|
return err(Errors.notFound('La invitación es inválida o ya venció.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
let acceptedCount = 0;
|
||||||
|
|
||||||
|
for (const invitation of invitations) {
|
||||||
|
await db.$transaction(async (tx) => {
|
||||||
|
const existingMembership = await tx.complexUser.findUnique({
|
||||||
|
where: {
|
||||||
|
complexId_userId: {
|
||||||
|
complexId: invitation.complexId,
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
select: { userId: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!existingMembership) {
|
||||||
|
await tx.complexUser.create({
|
||||||
|
data: {
|
||||||
|
complexId: invitation.complexId,
|
||||||
|
userId,
|
||||||
|
role: 'EMPLOYEE',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await tx.complexInvitation.update({
|
||||||
|
where: { id: invitation.id },
|
||||||
|
data: { acceptedAt: new Date() },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
acceptedCount += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ok({ acceptedCount });
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { handleResult } from '@/lib/http/handle-result';
|
||||||
|
import type { AppContext } from '@/types/hono';
|
||||||
|
import type { AcceptComplexInvitationsInput } from '@repo/api-contract';
|
||||||
|
import { acceptComplexInvitations } from './accept-complex-invitations.business';
|
||||||
|
|
||||||
|
export async function acceptComplexInvitationsHandler(c: AppContext) {
|
||||||
|
const user = c.get('user');
|
||||||
|
const payload = c.req.valid('json' as never) as AcceptComplexInvitationsInput;
|
||||||
|
|
||||||
|
return handleResult(c, await acceptComplexInvitations(user.id, user.email, payload));
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { Errors } from '@/lib/errors';
|
||||||
|
import { db } from '@/lib/prisma';
|
||||||
|
import { type Result, err, ok } from '@/lib/result';
|
||||||
|
import type { CancelComplexInvitationResponse } from '@repo/api-contract';
|
||||||
|
import { ensureComplexAdmin } from '../../shared/members-helpers';
|
||||||
|
|
||||||
|
export async function cancelComplexInvitation(
|
||||||
|
userId: string,
|
||||||
|
complexId: string,
|
||||||
|
invitationId: string
|
||||||
|
): Promise<Result<CancelComplexInvitationResponse>> {
|
||||||
|
const adminResult = await ensureComplexAdmin(userId, complexId);
|
||||||
|
if (!adminResult.ok) return adminResult;
|
||||||
|
|
||||||
|
const invitation = await db.complexInvitation.findUnique({
|
||||||
|
where: { id: invitationId },
|
||||||
|
select: { complexId: true, acceptedAt: true, revokedAt: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!invitation || invitation.complexId !== complexId) {
|
||||||
|
return err(Errors.notFound('La invitación no existe.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (invitation.acceptedAt) {
|
||||||
|
return err(Errors.conflict('La invitación ya fue aceptada.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (invitation.revokedAt) {
|
||||||
|
return ok({ ok: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.complexInvitation.update({
|
||||||
|
where: { id: invitationId },
|
||||||
|
data: { revokedAt: new Date() },
|
||||||
|
});
|
||||||
|
|
||||||
|
return ok({ ok: true });
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { handleResult } from '@/lib/http/handle-result';
|
||||||
|
import type { AppContext } from '@/types/hono';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { cancelComplexInvitation } from './cancel-complex-invitation.business';
|
||||||
|
|
||||||
|
const cancelInvitationParamsSchema = z.object({ id: z.uuid(), invitationId: z.uuid() });
|
||||||
|
|
||||||
|
export async function cancelComplexInvitationHandler(c: AppContext) {
|
||||||
|
const user = c.get('user');
|
||||||
|
const params = cancelInvitationParamsSchema.parse(c.req.param());
|
||||||
|
|
||||||
|
return handleResult(c, await cancelComplexInvitation(user.id, params.id, params.invitationId));
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import { db } from '@/lib/prisma';
|
||||||
|
import { buildUniqueSlug, slugify } from '@/lib/slug';
|
||||||
|
import { ensureActiveSport } from '@/modules/court/shared/guards';
|
||||||
|
import type { CreateComplexInput } from '@repo/api-contract';
|
||||||
|
import { v7 as uuidv7 } from 'uuid';
|
||||||
|
|
||||||
|
type DayOfWeek = 'MONDAY' | 'TUESDAY' | 'WEDNESDAY' | 'THURSDAY' | 'FRIDAY' | 'SATURDAY' | 'SUNDAY';
|
||||||
|
|
||||||
|
type CreateComplexInternalInput = CreateComplexInput & {
|
||||||
|
adminEmail: string;
|
||||||
|
userId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function toMinutes(value: string): number {
|
||||||
|
const [hours, minutes] = value.split(':').map((part) => Number(part));
|
||||||
|
return hours * 60 + minutes;
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertAvailabilityRanges(
|
||||||
|
availability: Array<{
|
||||||
|
dayOfWeek: DayOfWeek;
|
||||||
|
startTime: string;
|
||||||
|
endTime: string;
|
||||||
|
}>
|
||||||
|
) {
|
||||||
|
for (const range of availability) {
|
||||||
|
const start = toMinutes(range.startTime);
|
||||||
|
const end = toMinutes(range.endTime);
|
||||||
|
if (start >= end) {
|
||||||
|
throw new Error(`El rango ${range.startTime}-${range.endTime} es inválido.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createComplex(input: CreateComplexInternalInput) {
|
||||||
|
return db.$transaction(async (tx) => {
|
||||||
|
const base = slugify(input.complexName);
|
||||||
|
const fallback = base.length > 0 ? base : `complex-${uuidv7().slice(0, 8)}`;
|
||||||
|
const complexSlug = await buildUniqueSlug(fallback, (slug) =>
|
||||||
|
tx.complex.findFirst({ where: { complexSlug: slug }, select: { id: true } })
|
||||||
|
);
|
||||||
|
|
||||||
|
const complex = await tx.complex.create({
|
||||||
|
data: {
|
||||||
|
id: uuidv7(),
|
||||||
|
complexName: input.complexName,
|
||||||
|
physicalAddress: input.physicalAddress.trim(),
|
||||||
|
city: input.city?.trim() || null,
|
||||||
|
state: input.state?.trim() || null,
|
||||||
|
country: input.country?.trim() || null,
|
||||||
|
complexSlug,
|
||||||
|
adminEmail: input.adminEmail,
|
||||||
|
planCode: input.planCode,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (input.userId) {
|
||||||
|
await tx.complexUser.create({
|
||||||
|
data: {
|
||||||
|
complexId: complex.id,
|
||||||
|
userId: input.userId,
|
||||||
|
role: 'ADMIN',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.setupCourts && input.courtSportId) {
|
||||||
|
const sport = await ensureActiveSport(input.courtSportId);
|
||||||
|
const availability = (input.courtDaysOfWeek ?? []).map((day) => ({
|
||||||
|
dayOfWeek: day as DayOfWeek,
|
||||||
|
startTime: input.courtStartTime ?? '08:00',
|
||||||
|
endTime: input.courtEndTime ?? '22:00',
|
||||||
|
}));
|
||||||
|
|
||||||
|
assertAvailabilityRanges(availability);
|
||||||
|
|
||||||
|
const court = await tx.court.create({
|
||||||
|
data: {
|
||||||
|
id: uuidv7(),
|
||||||
|
complexId: complex.id,
|
||||||
|
sportId: input.courtSportId,
|
||||||
|
name: `${sport.name} 1`,
|
||||||
|
slotDurationMinutes: 60,
|
||||||
|
basePrice: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await tx.courtAvailability.createMany({
|
||||||
|
data: availability.map((avail) => ({
|
||||||
|
id: uuidv7(),
|
||||||
|
courtId: court.id,
|
||||||
|
dayOfWeek: avail.dayOfWeek,
|
||||||
|
startTime: avail.startTime,
|
||||||
|
endTime: avail.endTime,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return complex;
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { db } from '@/lib/prisma';
|
import { db } from '@/lib/prisma';
|
||||||
import { createComplex } from '@/modules/complex/services/complex.service';
|
|
||||||
import type { AppContext } from '@/types/hono';
|
import type { AppContext } from '@/types/hono';
|
||||||
import type { CreateComplexInput } from '@repo/api-contract';
|
import type { CreateComplexInput } from '@repo/api-contract';
|
||||||
|
import { createComplex } from './create-complex.business';
|
||||||
|
|
||||||
export async function createComplexHandler(c: AppContext) {
|
export async function createComplexHandler(c: AppContext) {
|
||||||
const payload = c.req.valid('json' as never) as CreateComplexInput;
|
const payload = c.req.valid('json' as never) as CreateComplexInput;
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { db } from '@/lib/prisma';
|
||||||
|
|
||||||
|
export async function getComplexById(id: string) {
|
||||||
|
return db.complex.findUnique({ where: { id } });
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { getComplexById } from '@/modules/complex/services/complex.service';
|
|
||||||
import type { AppContext } from '@/types/hono';
|
import type { AppContext } from '@/types/hono';
|
||||||
|
import { getComplexById } from './get-complex-by-id.business';
|
||||||
|
|
||||||
type ComplexIdParams = { id: string };
|
type ComplexIdParams = { id: string };
|
||||||
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { db } from '@/lib/prisma';
|
||||||
|
|
||||||
|
export async function getComplexBySlug(slug: string) {
|
||||||
|
return db.complex.findUnique({ where: { complexSlug: slug } });
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { getComplexBySlug } from '@/modules/complex/services/complex.service';
|
|
||||||
import type { AppContext } from '@/types/hono';
|
import type { AppContext } from '@/types/hono';
|
||||||
|
import { getComplexBySlug } from './get-complex-by-slug.business';
|
||||||
|
|
||||||
type ComplexSlugParams = { slug: string };
|
type ComplexSlugParams = { slug: string };
|
||||||
|
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { db } from '@/lib/prisma';
|
||||||
|
import { parsePlanRules } from '@/modules/plan/services/plan-rules.service';
|
||||||
|
|
||||||
|
export async function getCurrentComplex(userId: string, complexId: string) {
|
||||||
|
const complexUser = await db.complexUser.findUnique({
|
||||||
|
where: {
|
||||||
|
complexId_userId: {
|
||||||
|
complexId,
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
complex: {
|
||||||
|
include: {
|
||||||
|
plan: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!complexUser) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...complexUser.complex,
|
||||||
|
role: complexUser.role,
|
||||||
|
planFeatures: complexUser.complex.plan
|
||||||
|
? parsePlanRules(complexUser.complex.plan.rules).features
|
||||||
|
: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function selectComplex(userId: string, complexId: string) {
|
||||||
|
return getCurrentComplex(userId, complexId);
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { getCurrentComplex } from '@/modules/complex/services/complex.service';
|
|
||||||
import type { AppContext } from '@/types/hono';
|
import type { AppContext } from '@/types/hono';
|
||||||
import { getCookie } from 'hono/cookie';
|
import { getCookie } from 'hono/cookie';
|
||||||
|
import { getCurrentComplex } from './get-current-complex.business';
|
||||||
|
|
||||||
const SELECTED_COMPLEX_COOKIE = 'selected-complex-id';
|
const SELECTED_COMPLEX_COOKIE = 'selected-complex-id';
|
||||||
|
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
import { Errors } from '@/lib/errors';
|
||||||
|
import { db } from '@/lib/prisma';
|
||||||
|
import { type Result, err, ok } from '@/lib/result';
|
||||||
|
import type { InviteComplexUserInput, InviteComplexUserResponse } from '@repo/api-contract';
|
||||||
|
import { v7 as uuidv7 } from 'uuid';
|
||||||
|
import {
|
||||||
|
createInvitationToken,
|
||||||
|
ensureComplexAdmin,
|
||||||
|
formatInvitation,
|
||||||
|
hashToken,
|
||||||
|
normalizeEmail,
|
||||||
|
nowPlusDays,
|
||||||
|
sendInvitationEmail,
|
||||||
|
} from '../../shared/members-helpers';
|
||||||
|
|
||||||
|
export async function inviteComplexUser(
|
||||||
|
userId: string,
|
||||||
|
complexId: string,
|
||||||
|
input: InviteComplexUserInput
|
||||||
|
): Promise<Result<InviteComplexUserResponse>> {
|
||||||
|
const adminResult = await ensureComplexAdmin(userId, complexId);
|
||||||
|
if (!adminResult.ok) return adminResult;
|
||||||
|
|
||||||
|
const email = normalizeEmail(input.email);
|
||||||
|
const complex = await db.complex.findUnique({
|
||||||
|
where: { id: complexId },
|
||||||
|
select: { complexName: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!complex) {
|
||||||
|
return err(Errors.notFound('El complejo no existe.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingUser = await db.user.findUnique({
|
||||||
|
where: { email },
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existingUser) {
|
||||||
|
const existingMembership = await db.complexUser.findUnique({
|
||||||
|
where: {
|
||||||
|
complexId_userId: {
|
||||||
|
complexId,
|
||||||
|
userId: existingUser.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
select: { userId: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existingMembership) {
|
||||||
|
return err(Errors.conflict('Ese usuario ya pertenece al complejo.'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = createInvitationToken();
|
||||||
|
|
||||||
|
const invitation = await db.$transaction(async (tx) => {
|
||||||
|
const pendingInvitation = await tx.complexInvitation.findFirst({
|
||||||
|
where: {
|
||||||
|
complexId,
|
||||||
|
email,
|
||||||
|
acceptedAt: null,
|
||||||
|
revokedAt: null,
|
||||||
|
expiresAt: { gte: new Date() },
|
||||||
|
},
|
||||||
|
orderBy: { createdAt: 'desc' },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (pendingInvitation) {
|
||||||
|
return tx.complexInvitation.update({
|
||||||
|
where: { id: pendingInvitation.id },
|
||||||
|
data: {
|
||||||
|
tokenHash: hashToken(token),
|
||||||
|
expiresAt: nowPlusDays(Number(Bun.env.COMPLEX_INVITATION_TTL_DAYS ?? 7)),
|
||||||
|
acceptedAt: null,
|
||||||
|
revokedAt: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return tx.complexInvitation.create({
|
||||||
|
data: {
|
||||||
|
id: uuidv7(),
|
||||||
|
complexId,
|
||||||
|
email,
|
||||||
|
tokenHash: hashToken(token),
|
||||||
|
expiresAt: nowPlusDays(Number(Bun.env.COMPLEX_INVITATION_TTL_DAYS ?? 7)),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await sendInvitationEmail({
|
||||||
|
complexName: complex.complexName,
|
||||||
|
inviteeEmail: email,
|
||||||
|
token,
|
||||||
|
});
|
||||||
|
|
||||||
|
return ok({ invitation: formatInvitation(invitation) });
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { handleResult } from '@/lib/http/handle-result';
|
||||||
|
import type { AppContext } from '@/types/hono';
|
||||||
|
import type { InviteComplexUserInput } from '@repo/api-contract';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { inviteComplexUser as inviteComplexUserBusiness } from './invite-complex-user.business';
|
||||||
|
|
||||||
|
const complexParamsSchema = z.object({ id: z.uuid() });
|
||||||
|
|
||||||
|
type Deps = {
|
||||||
|
inviteComplexUser: typeof inviteComplexUserBusiness;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function createInviteComplexUserHandler(
|
||||||
|
deps: Deps = { inviteComplexUser: inviteComplexUserBusiness }
|
||||||
|
) {
|
||||||
|
return async function inviteComplexUserHandler(c: AppContext) {
|
||||||
|
const user = c.get('user');
|
||||||
|
const params = complexParamsSchema.parse(c.req.param());
|
||||||
|
const payload = c.req.valid('json' as never) as InviteComplexUserInput;
|
||||||
|
|
||||||
|
return handleResult(c, await deps.inviteComplexUser(user.id, params.id, payload), 201);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const inviteComplexUserHandler = createInviteComplexUserHandler();
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { resolveAvatarUrl } from '@/lib/avatar';
|
||||||
|
import { db } from '@/lib/prisma';
|
||||||
|
import { ensureComplexMember, formatInvitation } from '../../shared/members-helpers';
|
||||||
|
|
||||||
|
export async function listComplexUsers(complexId: string) {
|
||||||
|
const [members, invitations] = await Promise.all([
|
||||||
|
db.complexUser.findMany({
|
||||||
|
where: { complexId },
|
||||||
|
include: { user: true },
|
||||||
|
orderBy: { createdAt: 'asc' },
|
||||||
|
}),
|
||||||
|
db.complexInvitation.findMany({
|
||||||
|
where: {
|
||||||
|
complexId,
|
||||||
|
acceptedAt: null,
|
||||||
|
revokedAt: null,
|
||||||
|
expiresAt: { gte: new Date() },
|
||||||
|
},
|
||||||
|
orderBy: { createdAt: 'desc' },
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const membersWithAvatars = members.map((member) => ({
|
||||||
|
userId: member.userId,
|
||||||
|
fullName: member.user.name,
|
||||||
|
email: member.user.email,
|
||||||
|
avatarUrl: resolveAvatarUrl({
|
||||||
|
email: member.user.email,
|
||||||
|
image: member.user.image,
|
||||||
|
}),
|
||||||
|
role: member.role,
|
||||||
|
createdAt: member.createdAt.toISOString(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
members: membersWithAvatars,
|
||||||
|
invitations: invitations.map(formatInvitation),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getComplexUsersOverview(userId: string, complexId: string) {
|
||||||
|
await ensureComplexMember(userId, complexId);
|
||||||
|
return listComplexUsers(complexId);
|
||||||
|
}
|
||||||
@@ -1,10 +1,8 @@
|
|||||||
import { getComplexUsersOverview } from '@/modules/complex/services/complex-members.service';
|
|
||||||
import type { AppContext } from '@/types/hono';
|
import type { AppContext } from '@/types/hono';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
import { getComplexUsersOverview } from './list-complex-users.business';
|
||||||
|
|
||||||
const complexParamsSchema = z.object({
|
const complexParamsSchema = z.object({ id: z.uuid() });
|
||||||
id: z.uuid(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export async function listComplexUsersHandler(c: AppContext) {
|
export async function listComplexUsersHandler(c: AppContext) {
|
||||||
const user = c.get('user');
|
const user = c.get('user');
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { db } from '@/lib/prisma';
|
||||||
|
import { parsePlanRules } from '@/modules/plan/services/plan-rules.service';
|
||||||
|
|
||||||
|
export async function listMyComplexes(userId: string) {
|
||||||
|
const complexUsers = await db.complexUser.findMany({
|
||||||
|
where: { userId },
|
||||||
|
include: {
|
||||||
|
complex: {
|
||||||
|
include: {
|
||||||
|
plan: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
orderBy: { createdAt: 'asc' },
|
||||||
|
});
|
||||||
|
|
||||||
|
return complexUsers.map((complexUser) => ({
|
||||||
|
...complexUser.complex,
|
||||||
|
role: complexUser.role,
|
||||||
|
planFeatures: complexUser.complex.plan
|
||||||
|
? parsePlanRules(complexUser.complex.plan.rules).features
|
||||||
|
: null,
|
||||||
|
}));
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { listMyComplexes } from '@/modules/complex/services/complex.service';
|
|
||||||
import type { AppContext } from '@/types/hono';
|
import type { AppContext } from '@/types/hono';
|
||||||
|
import { listMyComplexes } from './list-my-complexes.business';
|
||||||
|
|
||||||
export async function listMyComplexesHandler(c: AppContext) {
|
export async function listMyComplexesHandler(c: AppContext) {
|
||||||
const user = c.get('user');
|
const user = c.get('user');
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import { Errors } from '@/lib/errors';
|
||||||
|
import { db } from '@/lib/prisma';
|
||||||
|
import { type Result, err, ok } from '@/lib/result';
|
||||||
|
import type { InviteComplexUserResponse } from '@repo/api-contract';
|
||||||
|
import {
|
||||||
|
ensureComplexAdmin,
|
||||||
|
formatInvitation,
|
||||||
|
refreshPendingInvitation,
|
||||||
|
sendInvitationEmail,
|
||||||
|
} from '../../shared/members-helpers';
|
||||||
|
|
||||||
|
export async function resendComplexInvitation(
|
||||||
|
userId: string,
|
||||||
|
complexId: string,
|
||||||
|
invitationId: string
|
||||||
|
): Promise<Result<InviteComplexUserResponse>> {
|
||||||
|
const adminResult = await ensureComplexAdmin(userId, complexId);
|
||||||
|
if (!adminResult.ok) return adminResult;
|
||||||
|
|
||||||
|
const pendingInvitation = await db.complexInvitation.findUnique({
|
||||||
|
where: { id: invitationId },
|
||||||
|
include: {
|
||||||
|
complex: { select: { complexName: true } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!pendingInvitation || pendingInvitation.complexId !== complexId) {
|
||||||
|
return err(Errors.notFound('La invitación no existe.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pendingInvitation.acceptedAt) {
|
||||||
|
return err(Errors.conflict('La invitación ya fue aceptada.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pendingInvitation.revokedAt) {
|
||||||
|
return err(Errors.conflict('La invitación fue cancelada.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const refreshed = await refreshPendingInvitation(invitationId);
|
||||||
|
|
||||||
|
await sendInvitationEmail({
|
||||||
|
complexName: refreshed.invitation.complex.complexName,
|
||||||
|
inviteeEmail: pendingInvitation.email,
|
||||||
|
token: refreshed.token,
|
||||||
|
});
|
||||||
|
|
||||||
|
return ok({
|
||||||
|
invitation: formatInvitation({
|
||||||
|
id: refreshed.invitation.id,
|
||||||
|
complexId: refreshed.invitation.complexId,
|
||||||
|
email: refreshed.invitation.email,
|
||||||
|
expiresAt: refreshed.invitation.expiresAt,
|
||||||
|
acceptedAt: refreshed.invitation.acceptedAt,
|
||||||
|
revokedAt: refreshed.invitation.revokedAt,
|
||||||
|
createdAt: refreshed.invitation.createdAt,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { handleResult } from '@/lib/http/handle-result';
|
||||||
|
import type { AppContext } from '@/types/hono';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { resendComplexInvitation } from './resend-complex-invitation.business';
|
||||||
|
|
||||||
|
const resendInvitationParamsSchema = z.object({ id: z.uuid(), invitationId: z.uuid() });
|
||||||
|
|
||||||
|
export async function resendComplexInvitationHandler(c: AppContext) {
|
||||||
|
const user = c.get('user');
|
||||||
|
const params = resendInvitationParamsSchema.parse(c.req.param());
|
||||||
|
|
||||||
|
return handleResult(c, await resendComplexInvitation(user.id, params.id, params.invitationId));
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { Errors } from '@/lib/errors';
|
||||||
|
import { db } from '@/lib/prisma';
|
||||||
|
import { type Result, err, ok } from '@/lib/result';
|
||||||
|
import type { RevokeComplexUserInput } from '@repo/api-contract';
|
||||||
|
import { ensureComplexAdmin } from '../../shared/members-helpers';
|
||||||
|
|
||||||
|
export async function revokeComplexUser(
|
||||||
|
userId: string,
|
||||||
|
complexId: string,
|
||||||
|
input: RevokeComplexUserInput
|
||||||
|
): Promise<Result<{ ok: boolean }>> {
|
||||||
|
const adminResult = await ensureComplexAdmin(userId, complexId);
|
||||||
|
if (!adminResult.ok) return adminResult;
|
||||||
|
|
||||||
|
const membership = await db.complexUser.findUnique({
|
||||||
|
where: {
|
||||||
|
complexId_userId: {
|
||||||
|
complexId,
|
||||||
|
userId: input.userId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
select: { role: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!membership) {
|
||||||
|
return err(Errors.notFound('Ese usuario no pertenece al complejo.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (membership.role !== 'EMPLOYEE') {
|
||||||
|
return err(Errors.conflict('Solo podés revocar usuarios con rol EMPLOYEE.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.complexUser.delete({
|
||||||
|
where: {
|
||||||
|
complexId_userId: {
|
||||||
|
complexId,
|
||||||
|
userId: input.userId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return ok({ ok: true });
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { handleResult } from '@/lib/http/handle-result';
|
||||||
|
import type { AppContext } from '@/types/hono';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { revokeComplexUser } from './revoke-complex-user.business';
|
||||||
|
|
||||||
|
const revokeParamsSchema = z.object({ id: z.uuid(), userId: z.string().min(1) });
|
||||||
|
|
||||||
|
export async function revokeComplexUserHandler(c: AppContext) {
|
||||||
|
const user = c.get('user');
|
||||||
|
const params = revokeParamsSchema.parse(c.req.param());
|
||||||
|
|
||||||
|
return handleResult(c, await revokeComplexUser(user.id, params.id, { userId: params.userId }));
|
||||||
|
}
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
import { selectComplex } from '@/modules/complex/services/complex.service';
|
import {
|
||||||
|
getCurrentComplex,
|
||||||
|
selectComplex,
|
||||||
|
} from '@/modules/complex/features/get-current-complex/get-current-complex.business';
|
||||||
import type { AppContext } from '@/types/hono';
|
import type { AppContext } from '@/types/hono';
|
||||||
import { setCookie } from 'hono/cookie';
|
import { setCookie } from 'hono/cookie';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import { Prisma } from '@/generated/prisma/client';
|
||||||
|
import { db } from '@/lib/prisma';
|
||||||
|
import { buildUniqueSlug, slugify } from '@/lib/slug';
|
||||||
|
import type { UpdateComplexInput } from '@repo/api-contract';
|
||||||
|
|
||||||
|
export async function getComplexById(id: string) {
|
||||||
|
return db.complex.findUnique({ where: { id } });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateComplex(id: string, input: UpdateComplexInput) {
|
||||||
|
const data: Record<string, unknown> = {};
|
||||||
|
|
||||||
|
if (input.complexName) {
|
||||||
|
data.complexName = input.complexName;
|
||||||
|
const base = slugify(input.complexName);
|
||||||
|
data.complexSlug = await buildUniqueSlug(base, (slug) =>
|
||||||
|
db.complex.findFirst({
|
||||||
|
where: { complexSlug: slug, id: { not: id } },
|
||||||
|
select: { id: true },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.complexSlug) {
|
||||||
|
const base = slugify(input.complexSlug);
|
||||||
|
data.complexSlug = await buildUniqueSlug(base, (slug) =>
|
||||||
|
db.complex.findFirst({
|
||||||
|
where: { complexSlug: slug, id: { not: id } },
|
||||||
|
select: { id: true },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.adminEmail) {
|
||||||
|
data.adminEmail = input.adminEmail;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.planCode !== undefined) {
|
||||||
|
data.planCode = input.planCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.physicalAddress !== undefined) {
|
||||||
|
data.physicalAddress = input.physicalAddress?.trim() ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.city !== undefined) {
|
||||||
|
data.city = input.city?.trim() ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.state !== undefined) {
|
||||||
|
data.state = input.state?.trim() ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.country !== undefined) {
|
||||||
|
data.country = input.country?.trim() ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return db.complex.update({
|
||||||
|
where: { id },
|
||||||
|
data: data as Prisma.ComplexUncheckedUpdateInput,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Prisma } from '@/generated/prisma/client';
|
import { Prisma } from '@/generated/prisma/client';
|
||||||
import { getComplexById, updateComplex } from '@/modules/complex/services/complex.service';
|
|
||||||
import type { AppContext } from '@/types/hono';
|
import type { AppContext } from '@/types/hono';
|
||||||
import type { UpdateComplexInput } from '@repo/api-contract';
|
import type { UpdateComplexInput } from '@repo/api-contract';
|
||||||
|
import { getComplexById, updateComplex } from './update-complex.business';
|
||||||
|
|
||||||
type ComplexIdParams = { id: string };
|
type ComplexIdParams = { id: string };
|
||||||
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import {
|
|
||||||
ComplexMembersError,
|
|
||||||
acceptComplexInvitations,
|
|
||||||
} from '@/modules/complex/services/complex-members.service';
|
|
||||||
import type { AppContext } from '@/types/hono';
|
|
||||||
import type { AcceptComplexInvitationsInput } from '@repo/api-contract';
|
|
||||||
|
|
||||||
export async function acceptComplexInvitationsHandler(c: AppContext) {
|
|
||||||
const user = c.get('user');
|
|
||||||
const payload = c.req.valid('json' as never) as AcceptComplexInvitationsInput;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await acceptComplexInvitations(user.id, user.email, payload);
|
|
||||||
return c.json(result);
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof ComplexMembersError) {
|
|
||||||
return c.json(
|
|
||||||
{ message: error.message },
|
|
||||||
{
|
|
||||||
status: error.status,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import {
|
|
||||||
ComplexMembersError,
|
|
||||||
cancelComplexInvitation,
|
|
||||||
} from '@/modules/complex/services/complex-members.service';
|
|
||||||
import type { AppContext } from '@/types/hono';
|
|
||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
const cancelInvitationParamsSchema = z.object({
|
|
||||||
id: z.uuid(),
|
|
||||||
invitationId: z.uuid(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export async function cancelComplexInvitationHandler(c: AppContext) {
|
|
||||||
const user = c.get('user');
|
|
||||||
const params = cancelInvitationParamsSchema.parse(c.req.param());
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await cancelComplexInvitation(user.id, params.id, params.invitationId);
|
|
||||||
return c.json(result);
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof ComplexMembersError) {
|
|
||||||
return c.json({ message: error.message }, { status: error.status });
|
|
||||||
}
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
import {
|
|
||||||
ComplexMembersError,
|
|
||||||
inviteComplexUser,
|
|
||||||
} from '@/modules/complex/services/complex-members.service';
|
|
||||||
import type { AppContext } from '@/types/hono';
|
|
||||||
import type { InviteComplexUserInput } from '@repo/api-contract';
|
|
||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
const complexParamsSchema = z.object({
|
|
||||||
id: z.uuid(),
|
|
||||||
});
|
|
||||||
|
|
||||||
type InviteComplexUserHandlerDeps = {
|
|
||||||
inviteComplexUser: typeof inviteComplexUser;
|
|
||||||
ComplexMembersError: typeof ComplexMembersError;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function createInviteComplexUserHandler(
|
|
||||||
deps: InviteComplexUserHandlerDeps = {
|
|
||||||
inviteComplexUser,
|
|
||||||
ComplexMembersError,
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
return async function inviteComplexUserHandler(c: AppContext) {
|
|
||||||
const user = c.get('user');
|
|
||||||
const params = complexParamsSchema.parse(c.req.param());
|
|
||||||
const payload = c.req.valid('json' as never) as InviteComplexUserInput;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await deps.inviteComplexUser(user.id, params.id, payload);
|
|
||||||
return c.json(result, 201);
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof deps.ComplexMembersError) {
|
|
||||||
return c.json(
|
|
||||||
{ message: error.message },
|
|
||||||
{
|
|
||||||
status: error.status,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const inviteComplexUserHandler = createInviteComplexUserHandler();
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import {
|
|
||||||
ComplexMembersError,
|
|
||||||
resendComplexInvitation,
|
|
||||||
} from '@/modules/complex/services/complex-members.service';
|
|
||||||
import type { AppContext } from '@/types/hono';
|
|
||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
const resendInvitationParamsSchema = z.object({
|
|
||||||
id: z.uuid(),
|
|
||||||
invitationId: z.uuid(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export async function resendComplexInvitationHandler(c: AppContext) {
|
|
||||||
const user = c.get('user');
|
|
||||||
const params = resendInvitationParamsSchema.parse(c.req.param());
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await resendComplexInvitation(user.id, params.id, params.invitationId);
|
|
||||||
return c.json(result);
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof ComplexMembersError) {
|
|
||||||
return c.json({ message: error.message }, { status: error.status });
|
|
||||||
}
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user