20 Commits

Author SHA1 Message Date
fd4d8b7abd Merge pull request 'feat/fixed-bookings' (#18) from feat/fixed-bookings into development
Reviewed-on: #18
2026-06-16 18:27:52 +00:00
Jose Selesan
3b3def94ab feat: add recurring booking management functionality
- Implemented API endpoints for listing and updating recurring booking groups.
- Added handlers for listing and updating recurring groups in the backend.
- Created frontend components for displaying and editing recurring bookings.
- Enhanced booking provider to manage recurring groups and their states.
- Updated API client to include new methods for recurring bookings.
- Introduced new validation schemas for updating recurring groups.
- Added UI elements for managing recurring bookings in the booking interface.
2026-06-16 15:16:38 -03:00
Jose Selesan
1318e3bf57 feat: add recurring bookings feature with related handlers and services
- Introduced recurring booking groups with the ability to create and cancel them.
- Added database migrations for recurring bookings, including new tables and relationships.
- Implemented handlers for creating and canceling recurring bookings in the admin booking module.
- Enhanced existing booking services to support recurring bookings logic.
- Updated API contract to include new schemas for recurring bookings.
- Refactored existing code for improved readability and maintainability.
2026-06-16 11:58:06 -03:00
Jose Selesan
1b5eb253f2 Updated AGENTS.md 2026-06-12 16:22:11 -03:00
ccee416b6f Merge pull request 'feat/admin-portal' (#17) from feat/admin-portal into development
Reviewed-on: #17
2026-06-10 16:08:57 +00:00
Jose Selesan
c3e26f8f0e feat(admin): add maxSports field to plan rules and limits schemas 2026-06-10 13:07:56 -03:00
Jose Selesan
ef926c63a2 feat(admin): implement pricing overrides for plans and update pricing logic 2026-06-10 11:22:57 -03:00
Jose Selesan
dc8a10a612 feat: add geolocation fields to session and implement geo stats endpoint
- Added country, city, countryCode, latitude, and longitude fields to the Session model.
- Updated geoip service to fetch and store geolocation data based on user IP.
- Created a new admin endpoint to retrieve geolocation statistics, including user counts by country and city.
- Enhanced admin page to display geolocation statistics with expandable city details.
- Introduced caching for geolocation data to optimize performance.
2026-06-10 10:18:31 -03:00
Jose Selesan
00f0cf511f feat(admin): add geolocation information to user sessions and implement geo IP fetching 2026-06-10 10:01:36 -03:00
Jose Selesan
93b3a82638 feat(admin): implement user search functionality in admin portal 2026-06-10 09:44:02 -03:00
Jose Selesan
c1c2f18471 Add admin portal 2026-06-10 09:39:20 -03:00
Jose Selesan
c278c78e3d feat(booking): replace filter toggle with dedicated list view for today's bookings 2026-06-08 23:21:35 -03:00
Jose Selesan
d767ac3ac7 fix(booking): prevent past time slots from being selectable for today 2026-06-08 22:50:56 -03:00
Jose Selesan
8c3ca0e7e1 Fix booking cancelation 2026-06-08 16:53:28 -03:00
Jose Selesan
72f273354e fix(public-booking): enhance focus behavior for customer name input on slot selection 2026-06-08 16:47:13 -03:00
Jose Selesan
ef2ded9d64 Fix input focus on time select 2026-06-08 16:41:24 -03:00
Jose Selesan
4c7b825129 refactor(public-booking): remove MobileBottomNav component and adjust layout styles 2026-06-08 16:34:36 -03:00
339e6a70d7 Merge pull request 'refactor(public-booking): remove CourtDetails component and simplify canGoBack logic' (#15) from feat/remove-attributes into development
Reviewed-on: #15
2026-06-08 19:16:04 +00:00
Jose Selesan
c70541b4f5 refactor(public-booking): remove CourtDetails component and simplify canGoBack logic 2026-06-08 16:14:27 -03:00
89673c3844 Merge pull request 'feat/court-maintenace' (#14) from feat/court-maintenace into development
Reviewed-on: #14
2026-06-08 18:47:02 +00:00
95 changed files with 7148 additions and 460 deletions

View File

@@ -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.

View File

@@ -1,3 +1,6 @@
# deps # deps
node_modules/ node_modules/
prisma.config.prod.ts prisma.config.prod.ts
# build output
dist/

View File

@@ -5,6 +5,9 @@ model User {
emailVerified Boolean @default(false) emailVerified Boolean @default(false)
image String? image String?
phone String? phone String?
banned Boolean @default(false)
bannedAt DateTime?
banReason String?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
sessions Session[] sessions Session[]
@@ -24,6 +27,11 @@ model Session {
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
ipAddress String? ipAddress String?
userAgent String? userAgent String?
country String?
city String?
countryCode String?
latitude Float?
longitude Float?
userId String userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade) user User @relation(fields: [userId], references: [id], onDelete: Cascade)

View File

@@ -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])

View File

@@ -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
@@ -34,6 +39,8 @@ model Court {
name String name String
slotDurationMinutes Int @map("slot_duration_minutes") slotDurationMinutes Int @map("slot_duration_minutes")
basePrice Decimal @map("base_price") @db.Decimal(19, 2) basePrice Decimal @map("base_price") @db.Decimal(19, 2)
isUnderMaintenance Boolean @default(false) @map("is_under_maintenance")
maintenanceReason String? @map("maintenance_reason") @db.VarChar(500)
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")
complex Complex @relation(fields: [complexId], references: [id], onDelete: Cascade) complex Complex @relation(fields: [complexId], references: [id], onDelete: Cascade)
@@ -41,12 +48,29 @@ model Court {
availabilities CourtAvailability[] availabilities CourtAvailability[]
priceRules CourtPriceRule[] priceRules CourtPriceRule[]
bookings CourtBooking[] bookings CourtBooking[]
maintenances CourtMaintenance[]
recurringGroups RecurringBookingGroup[]
@@index([complexId]) @@index([complexId])
@@index([sportId]) @@index([sportId])
@@map("courts") @@map("courts")
} }
model CourtMaintenance {
id String @id @db.Uuid
courtId String @map("court_id") @db.Uuid
startDate DateTime @map("start_date") @db.Date
startTime String? @map("start_time") @db.VarChar(5)
endTime String? @map("end_time") @db.VarChar(5)
reason String? @db.VarChar(500)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
court Court @relation(fields: [courtId], references: [id], onDelete: Cascade)
@@index([courtId, startDate])
@@map("court_maintenances")
}
model CourtAvailability { model CourtAvailability {
id String @id @db.Uuid id String @id @db.Uuid
courtId String @map("court_id") @db.Uuid courtId String @map("court_id") @db.Uuid
@@ -87,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")
} }
@@ -114,3 +141,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")
}

View File

@@ -0,0 +1,23 @@
-- CreateTable
CREATE TABLE "court_maintenances" (
"id" UUID NOT NULL,
"court_id" UUID NOT NULL,
"start_date" DATE NOT NULL,
"start_time" VARCHAR(5),
"end_time" VARCHAR(5),
"reason" VARCHAR(500),
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "court_maintenances_pkey" PRIMARY KEY ("id")
);
-- AlterTable
ALTER TABLE "courts" ADD COLUMN "is_under_maintenance" BOOLEAN NOT NULL DEFAULT false;
ALTER TABLE "courts" ADD COLUMN "maintenance_reason" VARCHAR(500);
-- CreateIndex
CREATE INDEX "court_maintenances_court_id_start_date_idx" ON "court_maintenances"("court_id", "start_date");
-- AddForeignKey
ALTER TABLE "court_maintenances" ADD CONSTRAINT "court_maintenances_court_id_fkey" FOREIGN KEY ("court_id") REFERENCES "courts"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -0,0 +1,4 @@
-- AlterTable
ALTER TABLE "users" ADD COLUMN "banReason" TEXT,
ADD COLUMN "banned" BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN "bannedAt" TIMESTAMP(3);

View File

@@ -0,0 +1,6 @@
-- AlterTable
ALTER TABLE "sessions" ADD COLUMN "city" TEXT,
ADD COLUMN "country" TEXT,
ADD COLUMN "countryCode" TEXT,
ADD COLUMN "latitude" DOUBLE PRECISION,
ADD COLUMN "longitude" DOUBLE PRECISION;

View File

@@ -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;

View File

@@ -4,7 +4,7 @@ export const planSeeds = [
name: 'Basic', name: 'Basic',
price: '49.00', price: '49.00',
rules: { rules: {
version: 'v1', version: 'v2',
limits: { limits: {
maxCourts: 2, maxCourts: 2,
maxBookingsPerDay: 80, maxBookingsPerDay: 80,
@@ -21,6 +21,12 @@ export const planSeeds = [
publicBookingPage: true, publicBookingPage: true,
advancedReports: false, advancedReports: false,
whatsappReminders: false, whatsappReminders: false,
fixedSlots: false,
},
pricing: {
overrides: {
AR: { amount: 14999, currency: 'ARS' },
},
}, },
}, },
}, },
@@ -29,7 +35,7 @@ export const planSeeds = [
name: 'Advanced', name: 'Advanced',
price: '99.00', price: '99.00',
rules: { rules: {
version: 'v1', version: 'v2',
limits: { limits: {
maxCourts: 5, maxCourts: 5,
maxBookingsPerDay: 220, maxBookingsPerDay: 220,
@@ -46,6 +52,12 @@ export const planSeeds = [
publicBookingPage: true, publicBookingPage: true,
advancedReports: true, advancedReports: true,
whatsappReminders: true, whatsappReminders: true,
fixedSlots: true,
},
pricing: {
overrides: {
AR: { amount: 29999, currency: 'ARS' },
},
}, },
}, },
}, },
@@ -54,7 +66,7 @@ export const planSeeds = [
name: 'Enterprise', name: 'Enterprise',
price: '199.00', price: '199.00',
rules: { rules: {
version: 'v1', version: 'v2',
limits: { limits: {
maxCourts: 20, maxCourts: 20,
maxBookingsPerDay: 2000, maxBookingsPerDay: 2000,
@@ -71,6 +83,12 @@ export const planSeeds = [
publicBookingPage: true, publicBookingPage: true,
advancedReports: true, advancedReports: true,
whatsappReminders: true, whatsappReminders: true,
fixedSlots: true,
},
pricing: {
overrides: {
AR: { amount: 59999, currency: 'ARS' },
},
}, },
}, },
}, },

View File

@@ -62,6 +62,11 @@ export type Sport = Prisma.SportModel
* *
*/ */
export type Court = Prisma.CourtModel export type Court = Prisma.CourtModel
/**
* Model CourtMaintenance
*
*/
export type CourtMaintenance = Prisma.CourtMaintenanceModel
/** /**
* Model CourtAvailability * Model CourtAvailability
* *
@@ -82,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
* *

View File

@@ -86,6 +86,11 @@ export type Sport = Prisma.SportModel
* *
*/ */
export type Court = Prisma.CourtModel export type Court = Prisma.CourtModel
/**
* Model CourtMaintenance
*
*/
export type CourtMaintenance = Prisma.CourtMaintenanceModel
/** /**
* Model CourtAvailability * Model CourtAvailability
* *
@@ -106,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
* *

View File

@@ -49,6 +49,17 @@ export type StringNullableFilter<$PrismaModel = never> = {
not?: Prisma.NestedStringNullableFilter<$PrismaModel> | string | null not?: Prisma.NestedStringNullableFilter<$PrismaModel> | string | null
} }
export type DateTimeNullableFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
not?: Prisma.NestedDateTimeNullableFilter<$PrismaModel> | Date | string | null
}
export type DateTimeFilter<$PrismaModel = never> = { export type DateTimeFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
@@ -109,6 +120,20 @@ export type StringNullableWithAggregatesFilter<$PrismaModel = never> = {
_max?: Prisma.NestedStringNullableFilter<$PrismaModel> _max?: Prisma.NestedStringNullableFilter<$PrismaModel>
} }
export type DateTimeNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
not?: Prisma.NestedDateTimeNullableWithAggregatesFilter<$PrismaModel> | Date | string | null
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
_min?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
_max?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
}
export type DateTimeWithAggregatesFilter<$PrismaModel = never> = { export type DateTimeWithAggregatesFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
@@ -123,29 +148,31 @@ export type DateTimeWithAggregatesFilter<$PrismaModel = never> = {
_max?: Prisma.NestedDateTimeFilter<$PrismaModel> _max?: Prisma.NestedDateTimeFilter<$PrismaModel>
} }
export type DateTimeNullableFilter<$PrismaModel = never> = { export type FloatNullableFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null equals?: number | Prisma.FloatFieldRefInput<$PrismaModel> | null
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null in?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel> | null
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null notIn?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel> | null
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> lt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> lte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> gt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> gte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
not?: Prisma.NestedDateTimeNullableFilter<$PrismaModel> | Date | string | null not?: Prisma.NestedFloatNullableFilter<$PrismaModel> | number | null
} }
export type DateTimeNullableWithAggregatesFilter<$PrismaModel = never> = { export type FloatNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null equals?: number | Prisma.FloatFieldRefInput<$PrismaModel> | null
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null in?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel> | null
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null notIn?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel> | null
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> lt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> lte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> gt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> gte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
not?: Prisma.NestedDateTimeNullableWithAggregatesFilter<$PrismaModel> | Date | string | null not?: Prisma.NestedFloatNullableWithAggregatesFilter<$PrismaModel> | number | null
_count?: Prisma.NestedIntNullableFilter<$PrismaModel> _count?: Prisma.NestedIntNullableFilter<$PrismaModel>
_min?: Prisma.NestedDateTimeNullableFilter<$PrismaModel> _avg?: Prisma.NestedFloatNullableFilter<$PrismaModel>
_max?: Prisma.NestedDateTimeNullableFilter<$PrismaModel> _sum?: Prisma.NestedFloatNullableFilter<$PrismaModel>
_min?: Prisma.NestedFloatNullableFilter<$PrismaModel>
_max?: Prisma.NestedFloatNullableFilter<$PrismaModel>
} }
export type UuidFilter<$PrismaModel = never> = { export type UuidFilter<$PrismaModel = never> = {
@@ -287,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>
@@ -297,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'>>,
@@ -381,6 +452,17 @@ export type NestedStringNullableFilter<$PrismaModel = never> = {
not?: Prisma.NestedStringNullableFilter<$PrismaModel> | string | null not?: Prisma.NestedStringNullableFilter<$PrismaModel> | string | null
} }
export type NestedDateTimeNullableFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
not?: Prisma.NestedDateTimeNullableFilter<$PrismaModel> | Date | string | null
}
export type NestedDateTimeFilter<$PrismaModel = never> = { export type NestedDateTimeFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
@@ -456,6 +538,20 @@ export type NestedIntNullableFilter<$PrismaModel = never> = {
not?: Prisma.NestedIntNullableFilter<$PrismaModel> | number | null not?: Prisma.NestedIntNullableFilter<$PrismaModel> | number | null
} }
export type NestedDateTimeNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
not?: Prisma.NestedDateTimeNullableWithAggregatesFilter<$PrismaModel> | Date | string | null
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
_min?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
_max?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
}
export type NestedDateTimeWithAggregatesFilter<$PrismaModel = never> = { export type NestedDateTimeWithAggregatesFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
@@ -470,29 +566,31 @@ export type NestedDateTimeWithAggregatesFilter<$PrismaModel = never> = {
_max?: Prisma.NestedDateTimeFilter<$PrismaModel> _max?: Prisma.NestedDateTimeFilter<$PrismaModel>
} }
export type NestedDateTimeNullableFilter<$PrismaModel = never> = { export type NestedFloatNullableFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null equals?: number | Prisma.FloatFieldRefInput<$PrismaModel> | null
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null in?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel> | null
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null notIn?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel> | null
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> lt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> lte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> gt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> gte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
not?: Prisma.NestedDateTimeNullableFilter<$PrismaModel> | Date | string | null not?: Prisma.NestedFloatNullableFilter<$PrismaModel> | number | null
} }
export type NestedDateTimeNullableWithAggregatesFilter<$PrismaModel = never> = { export type NestedFloatNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null equals?: number | Prisma.FloatFieldRefInput<$PrismaModel> | null
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null in?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel> | null
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null notIn?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel> | null
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> lt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> lte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> gt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> gte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
not?: Prisma.NestedDateTimeNullableWithAggregatesFilter<$PrismaModel> | Date | string | null not?: Prisma.NestedFloatNullableWithAggregatesFilter<$PrismaModel> | number | null
_count?: Prisma.NestedIntNullableFilter<$PrismaModel> _count?: Prisma.NestedIntNullableFilter<$PrismaModel>
_min?: Prisma.NestedDateTimeNullableFilter<$PrismaModel> _avg?: Prisma.NestedFloatNullableFilter<$PrismaModel>
_max?: Prisma.NestedDateTimeNullableFilter<$PrismaModel> _sum?: Prisma.NestedFloatNullableFilter<$PrismaModel>
_min?: Prisma.NestedFloatNullableFilter<$PrismaModel>
_max?: Prisma.NestedFloatNullableFilter<$PrismaModel>
} }
export type NestedUuidFilter<$PrismaModel = never> = { export type NestedUuidFilter<$PrismaModel = never> = {
@@ -632,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>
@@ -642,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'>>,

View File

@@ -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

View File

@@ -393,10 +393,12 @@ export const ModelName = {
ComplexInvitation: 'ComplexInvitation', ComplexInvitation: 'ComplexInvitation',
Sport: 'Sport', Sport: 'Sport',
Court: 'Court', Court: 'Court',
CourtMaintenance: 'CourtMaintenance',
CourtAvailability: 'CourtAvailability', CourtAvailability: 'CourtAvailability',
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
@@ -414,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" | "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: {
@@ -1084,6 +1086,80 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
} }
} }
} }
CourtMaintenance: {
payload: Prisma.$CourtMaintenancePayload<ExtArgs>
fields: Prisma.CourtMaintenanceFieldRefs
operations: {
findUnique: {
args: Prisma.CourtMaintenanceFindUniqueArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$CourtMaintenancePayload> | null
}
findUniqueOrThrow: {
args: Prisma.CourtMaintenanceFindUniqueOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$CourtMaintenancePayload>
}
findFirst: {
args: Prisma.CourtMaintenanceFindFirstArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$CourtMaintenancePayload> | null
}
findFirstOrThrow: {
args: Prisma.CourtMaintenanceFindFirstOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$CourtMaintenancePayload>
}
findMany: {
args: Prisma.CourtMaintenanceFindManyArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$CourtMaintenancePayload>[]
}
create: {
args: Prisma.CourtMaintenanceCreateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$CourtMaintenancePayload>
}
createMany: {
args: Prisma.CourtMaintenanceCreateManyArgs<ExtArgs>
result: BatchPayload
}
createManyAndReturn: {
args: Prisma.CourtMaintenanceCreateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$CourtMaintenancePayload>[]
}
delete: {
args: Prisma.CourtMaintenanceDeleteArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$CourtMaintenancePayload>
}
update: {
args: Prisma.CourtMaintenanceUpdateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$CourtMaintenancePayload>
}
deleteMany: {
args: Prisma.CourtMaintenanceDeleteManyArgs<ExtArgs>
result: BatchPayload
}
updateMany: {
args: Prisma.CourtMaintenanceUpdateManyArgs<ExtArgs>
result: BatchPayload
}
updateManyAndReturn: {
args: Prisma.CourtMaintenanceUpdateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$CourtMaintenancePayload>[]
}
upsert: {
args: Prisma.CourtMaintenanceUpsertArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$CourtMaintenancePayload>
}
aggregate: {
args: Prisma.CourtMaintenanceAggregateArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.AggregateCourtMaintenance>
}
groupBy: {
args: Prisma.CourtMaintenanceGroupByArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.CourtMaintenanceGroupByOutputType>[]
}
count: {
args: Prisma.CourtMaintenanceCountArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.CourtMaintenanceCountAggregateOutputType> | number
}
}
}
CourtAvailability: { CourtAvailability: {
payload: Prisma.$CourtAvailabilityPayload<ExtArgs> payload: Prisma.$CourtAvailabilityPayload<ExtArgs>
fields: Prisma.CourtAvailabilityFieldRefs fields: Prisma.CourtAvailabilityFieldRefs
@@ -1380,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
@@ -1574,6 +1724,9 @@ export const UserScalarFieldEnum = {
emailVerified: 'emailVerified', emailVerified: 'emailVerified',
image: 'image', image: 'image',
phone: 'phone', phone: 'phone',
banned: 'banned',
bannedAt: 'bannedAt',
banReason: 'banReason',
createdAt: 'createdAt', createdAt: 'createdAt',
updatedAt: 'updatedAt', updatedAt: 'updatedAt',
role: 'role' role: 'role'
@@ -1590,6 +1743,11 @@ export const SessionScalarFieldEnum = {
updatedAt: 'updatedAt', updatedAt: 'updatedAt',
ipAddress: 'ipAddress', ipAddress: 'ipAddress',
userAgent: 'userAgent', userAgent: 'userAgent',
country: 'country',
city: 'city',
countryCode: 'countryCode',
latitude: 'latitude',
longitude: 'longitude',
userId: 'userId' userId: 'userId'
} as const } as const
@@ -1688,6 +1846,8 @@ export const CourtScalarFieldEnum = {
name: 'name', name: 'name',
slotDurationMinutes: 'slotDurationMinutes', slotDurationMinutes: 'slotDurationMinutes',
basePrice: 'basePrice', basePrice: 'basePrice',
isUnderMaintenance: 'isUnderMaintenance',
maintenanceReason: 'maintenanceReason',
createdAt: 'createdAt', createdAt: 'createdAt',
updatedAt: 'updatedAt' updatedAt: 'updatedAt'
} as const } as const
@@ -1695,6 +1855,20 @@ export const CourtScalarFieldEnum = {
export type CourtScalarFieldEnum = (typeof CourtScalarFieldEnum)[keyof typeof CourtScalarFieldEnum] export type CourtScalarFieldEnum = (typeof CourtScalarFieldEnum)[keyof typeof CourtScalarFieldEnum]
export const CourtMaintenanceScalarFieldEnum = {
id: 'id',
courtId: 'courtId',
startDate: 'startDate',
startTime: 'startTime',
endTime: 'endTime',
reason: 'reason',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
export type CourtMaintenanceScalarFieldEnum = (typeof CourtMaintenanceScalarFieldEnum)[keyof typeof CourtMaintenanceScalarFieldEnum]
export const CourtAvailabilityScalarFieldEnum = { export const CourtAvailabilityScalarFieldEnum = {
id: 'id', id: 'id',
courtId: 'courtId', courtId: 'courtId',
@@ -1733,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
@@ -1758,6 +1933,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',
@@ -1866,6 +2061,20 @@ export type ListDateTimeFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaM
/**
* Reference to a field of type 'Float'
*/
export type FloatFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'Float'>
/**
* Reference to a field of type 'Float[]'
*/
export type ListFloatFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'Float[]'>
/** /**
* Reference to a field of type 'ComplexUserRole' * Reference to a field of type 'ComplexUserRole'
*/ */
@@ -1936,6 +2145,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'
*/ */
@@ -1949,20 +2172,6 @@ export type JsonFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'J
export type EnumQueryModeFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'QueryMode'> export type EnumQueryModeFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'QueryMode'>
/**
* Reference to a field of type 'Float'
*/
export type FloatFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'Float'>
/**
* Reference to a field of type 'Float[]'
*/
export type ListFloatFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'Float[]'>
/** /**
* Batch Payload for updateMany & deleteMany & createMany * Batch Payload for updateMany & deleteMany & createMany
*/ */
@@ -2067,10 +2276,12 @@ export type GlobalOmitConfig = {
complexInvitation?: Prisma.ComplexInvitationOmit complexInvitation?: Prisma.ComplexInvitationOmit
sport?: Prisma.SportOmit sport?: Prisma.SportOmit
court?: Prisma.CourtOmit court?: Prisma.CourtOmit
courtMaintenance?: Prisma.CourtMaintenanceOmit
courtAvailability?: Prisma.CourtAvailabilityOmit courtAvailability?: Prisma.CourtAvailabilityOmit
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
} }

View File

@@ -60,10 +60,12 @@ export const ModelName = {
ComplexInvitation: 'ComplexInvitation', ComplexInvitation: 'ComplexInvitation',
Sport: 'Sport', Sport: 'Sport',
Court: 'Court', Court: 'Court',
CourtMaintenance: 'CourtMaintenance',
CourtAvailability: 'CourtAvailability', CourtAvailability: 'CourtAvailability',
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
@@ -91,6 +93,9 @@ export const UserScalarFieldEnum = {
emailVerified: 'emailVerified', emailVerified: 'emailVerified',
image: 'image', image: 'image',
phone: 'phone', phone: 'phone',
banned: 'banned',
bannedAt: 'bannedAt',
banReason: 'banReason',
createdAt: 'createdAt', createdAt: 'createdAt',
updatedAt: 'updatedAt', updatedAt: 'updatedAt',
role: 'role' role: 'role'
@@ -107,6 +112,11 @@ export const SessionScalarFieldEnum = {
updatedAt: 'updatedAt', updatedAt: 'updatedAt',
ipAddress: 'ipAddress', ipAddress: 'ipAddress',
userAgent: 'userAgent', userAgent: 'userAgent',
country: 'country',
city: 'city',
countryCode: 'countryCode',
latitude: 'latitude',
longitude: 'longitude',
userId: 'userId' userId: 'userId'
} as const } as const
@@ -205,6 +215,8 @@ export const CourtScalarFieldEnum = {
name: 'name', name: 'name',
slotDurationMinutes: 'slotDurationMinutes', slotDurationMinutes: 'slotDurationMinutes',
basePrice: 'basePrice', basePrice: 'basePrice',
isUnderMaintenance: 'isUnderMaintenance',
maintenanceReason: 'maintenanceReason',
createdAt: 'createdAt', createdAt: 'createdAt',
updatedAt: 'updatedAt' updatedAt: 'updatedAt'
} as const } as const
@@ -212,6 +224,20 @@ export const CourtScalarFieldEnum = {
export type CourtScalarFieldEnum = (typeof CourtScalarFieldEnum)[keyof typeof CourtScalarFieldEnum] export type CourtScalarFieldEnum = (typeof CourtScalarFieldEnum)[keyof typeof CourtScalarFieldEnum]
export const CourtMaintenanceScalarFieldEnum = {
id: 'id',
courtId: 'courtId',
startDate: 'startDate',
startTime: 'startTime',
endTime: 'endTime',
reason: 'reason',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
export type CourtMaintenanceScalarFieldEnum = (typeof CourtMaintenanceScalarFieldEnum)[keyof typeof CourtMaintenanceScalarFieldEnum]
export const CourtAvailabilityScalarFieldEnum = { export const CourtAvailabilityScalarFieldEnum = {
id: 'id', id: 'id',
courtId: 'courtId', courtId: 'courtId',
@@ -250,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
@@ -275,6 +302,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',

View File

@@ -17,10 +17,12 @@ export type * from './models/ComplexUser'
export type * from './models/ComplexInvitation' export type * from './models/ComplexInvitation'
export type * from './models/Sport' export type * from './models/Sport'
export type * from './models/Court' export type * from './models/Court'
export type * from './models/CourtMaintenance'
export type * from './models/CourtAvailability' 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'

View File

@@ -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
*/ */

View File

@@ -43,6 +43,8 @@ export type CourtMinAggregateOutputType = {
name: string | null name: string | null
slotDurationMinutes: number | null slotDurationMinutes: number | null
basePrice: runtime.Decimal | null basePrice: runtime.Decimal | null
isUnderMaintenance: boolean | null
maintenanceReason: string | null
createdAt: Date | null createdAt: Date | null
updatedAt: Date | null updatedAt: Date | null
} }
@@ -54,6 +56,8 @@ export type CourtMaxAggregateOutputType = {
name: string | null name: string | null
slotDurationMinutes: number | null slotDurationMinutes: number | null
basePrice: runtime.Decimal | null basePrice: runtime.Decimal | null
isUnderMaintenance: boolean | null
maintenanceReason: string | null
createdAt: Date | null createdAt: Date | null
updatedAt: Date | null updatedAt: Date | null
} }
@@ -65,6 +69,8 @@ export type CourtCountAggregateOutputType = {
name: number name: number
slotDurationMinutes: number slotDurationMinutes: number
basePrice: number basePrice: number
isUnderMaintenance: number
maintenanceReason: number
createdAt: number createdAt: number
updatedAt: number updatedAt: number
_all: number _all: number
@@ -88,6 +94,8 @@ export type CourtMinAggregateInputType = {
name?: true name?: true
slotDurationMinutes?: true slotDurationMinutes?: true
basePrice?: true basePrice?: true
isUnderMaintenance?: true
maintenanceReason?: true
createdAt?: true createdAt?: true
updatedAt?: true updatedAt?: true
} }
@@ -99,6 +107,8 @@ export type CourtMaxAggregateInputType = {
name?: true name?: true
slotDurationMinutes?: true slotDurationMinutes?: true
basePrice?: true basePrice?: true
isUnderMaintenance?: true
maintenanceReason?: true
createdAt?: true createdAt?: true
updatedAt?: true updatedAt?: true
} }
@@ -110,6 +120,8 @@ export type CourtCountAggregateInputType = {
name?: true name?: true
slotDurationMinutes?: true slotDurationMinutes?: true
basePrice?: true basePrice?: true
isUnderMaintenance?: true
maintenanceReason?: true
createdAt?: true createdAt?: true
updatedAt?: true updatedAt?: true
_all?: true _all?: true
@@ -208,6 +220,8 @@ export type CourtGroupByOutputType = {
name: string name: string
slotDurationMinutes: number slotDurationMinutes: number
basePrice: runtime.Decimal basePrice: runtime.Decimal
isUnderMaintenance: boolean
maintenanceReason: string | null
createdAt: Date createdAt: Date
updatedAt: Date updatedAt: Date
_count: CourtCountAggregateOutputType | null _count: CourtCountAggregateOutputType | null
@@ -242,6 +256,8 @@ export type CourtWhereInput = {
name?: Prisma.StringFilter<"Court"> | string name?: Prisma.StringFilter<"Court"> | string
slotDurationMinutes?: Prisma.IntFilter<"Court"> | number slotDurationMinutes?: Prisma.IntFilter<"Court"> | number
basePrice?: Prisma.DecimalFilter<"Court"> | runtime.Decimal | runtime.DecimalJsLike | number | string basePrice?: Prisma.DecimalFilter<"Court"> | runtime.Decimal | runtime.DecimalJsLike | number | string
isUnderMaintenance?: Prisma.BoolFilter<"Court"> | boolean
maintenanceReason?: Prisma.StringNullableFilter<"Court"> | string | null
createdAt?: Prisma.DateTimeFilter<"Court"> | Date | string createdAt?: Prisma.DateTimeFilter<"Court"> | Date | string
updatedAt?: Prisma.DateTimeFilter<"Court"> | Date | string updatedAt?: Prisma.DateTimeFilter<"Court"> | Date | string
complex?: Prisma.XOR<Prisma.ComplexScalarRelationFilter, Prisma.ComplexWhereInput> complex?: Prisma.XOR<Prisma.ComplexScalarRelationFilter, Prisma.ComplexWhereInput>
@@ -249,6 +265,8 @@ export type CourtWhereInput = {
availabilities?: Prisma.CourtAvailabilityListRelationFilter availabilities?: Prisma.CourtAvailabilityListRelationFilter
priceRules?: Prisma.CourtPriceRuleListRelationFilter priceRules?: Prisma.CourtPriceRuleListRelationFilter
bookings?: Prisma.CourtBookingListRelationFilter bookings?: Prisma.CourtBookingListRelationFilter
maintenances?: Prisma.CourtMaintenanceListRelationFilter
recurringGroups?: Prisma.RecurringBookingGroupListRelationFilter
} }
export type CourtOrderByWithRelationInput = { export type CourtOrderByWithRelationInput = {
@@ -258,6 +276,8 @@ export type CourtOrderByWithRelationInput = {
name?: Prisma.SortOrder name?: Prisma.SortOrder
slotDurationMinutes?: Prisma.SortOrder slotDurationMinutes?: Prisma.SortOrder
basePrice?: Prisma.SortOrder basePrice?: Prisma.SortOrder
isUnderMaintenance?: Prisma.SortOrder
maintenanceReason?: Prisma.SortOrderInput | Prisma.SortOrder
createdAt?: Prisma.SortOrder createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder updatedAt?: Prisma.SortOrder
complex?: Prisma.ComplexOrderByWithRelationInput complex?: Prisma.ComplexOrderByWithRelationInput
@@ -265,6 +285,8 @@ export type CourtOrderByWithRelationInput = {
availabilities?: Prisma.CourtAvailabilityOrderByRelationAggregateInput availabilities?: Prisma.CourtAvailabilityOrderByRelationAggregateInput
priceRules?: Prisma.CourtPriceRuleOrderByRelationAggregateInput priceRules?: Prisma.CourtPriceRuleOrderByRelationAggregateInput
bookings?: Prisma.CourtBookingOrderByRelationAggregateInput bookings?: Prisma.CourtBookingOrderByRelationAggregateInput
maintenances?: Prisma.CourtMaintenanceOrderByRelationAggregateInput
recurringGroups?: Prisma.RecurringBookingGroupOrderByRelationAggregateInput
} }
export type CourtWhereUniqueInput = Prisma.AtLeast<{ export type CourtWhereUniqueInput = Prisma.AtLeast<{
@@ -277,6 +299,8 @@ export type CourtWhereUniqueInput = Prisma.AtLeast<{
name?: Prisma.StringFilter<"Court"> | string name?: Prisma.StringFilter<"Court"> | string
slotDurationMinutes?: Prisma.IntFilter<"Court"> | number slotDurationMinutes?: Prisma.IntFilter<"Court"> | number
basePrice?: Prisma.DecimalFilter<"Court"> | runtime.Decimal | runtime.DecimalJsLike | number | string basePrice?: Prisma.DecimalFilter<"Court"> | runtime.Decimal | runtime.DecimalJsLike | number | string
isUnderMaintenance?: Prisma.BoolFilter<"Court"> | boolean
maintenanceReason?: Prisma.StringNullableFilter<"Court"> | string | null
createdAt?: Prisma.DateTimeFilter<"Court"> | Date | string createdAt?: Prisma.DateTimeFilter<"Court"> | Date | string
updatedAt?: Prisma.DateTimeFilter<"Court"> | Date | string updatedAt?: Prisma.DateTimeFilter<"Court"> | Date | string
complex?: Prisma.XOR<Prisma.ComplexScalarRelationFilter, Prisma.ComplexWhereInput> complex?: Prisma.XOR<Prisma.ComplexScalarRelationFilter, Prisma.ComplexWhereInput>
@@ -284,6 +308,8 @@ export type CourtWhereUniqueInput = Prisma.AtLeast<{
availabilities?: Prisma.CourtAvailabilityListRelationFilter availabilities?: Prisma.CourtAvailabilityListRelationFilter
priceRules?: Prisma.CourtPriceRuleListRelationFilter priceRules?: Prisma.CourtPriceRuleListRelationFilter
bookings?: Prisma.CourtBookingListRelationFilter bookings?: Prisma.CourtBookingListRelationFilter
maintenances?: Prisma.CourtMaintenanceListRelationFilter
recurringGroups?: Prisma.RecurringBookingGroupListRelationFilter
}, "id"> }, "id">
export type CourtOrderByWithAggregationInput = { export type CourtOrderByWithAggregationInput = {
@@ -293,6 +319,8 @@ export type CourtOrderByWithAggregationInput = {
name?: Prisma.SortOrder name?: Prisma.SortOrder
slotDurationMinutes?: Prisma.SortOrder slotDurationMinutes?: Prisma.SortOrder
basePrice?: Prisma.SortOrder basePrice?: Prisma.SortOrder
isUnderMaintenance?: Prisma.SortOrder
maintenanceReason?: Prisma.SortOrderInput | Prisma.SortOrder
createdAt?: Prisma.SortOrder createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder updatedAt?: Prisma.SortOrder
_count?: Prisma.CourtCountOrderByAggregateInput _count?: Prisma.CourtCountOrderByAggregateInput
@@ -312,6 +340,8 @@ export type CourtScalarWhereWithAggregatesInput = {
name?: Prisma.StringWithAggregatesFilter<"Court"> | string name?: Prisma.StringWithAggregatesFilter<"Court"> | string
slotDurationMinutes?: Prisma.IntWithAggregatesFilter<"Court"> | number slotDurationMinutes?: Prisma.IntWithAggregatesFilter<"Court"> | number
basePrice?: Prisma.DecimalWithAggregatesFilter<"Court"> | runtime.Decimal | runtime.DecimalJsLike | number | string basePrice?: Prisma.DecimalWithAggregatesFilter<"Court"> | runtime.Decimal | runtime.DecimalJsLike | number | string
isUnderMaintenance?: Prisma.BoolWithAggregatesFilter<"Court"> | boolean
maintenanceReason?: Prisma.StringNullableWithAggregatesFilter<"Court"> | string | null
createdAt?: Prisma.DateTimeWithAggregatesFilter<"Court"> | Date | string createdAt?: Prisma.DateTimeWithAggregatesFilter<"Court"> | Date | string
updatedAt?: Prisma.DateTimeWithAggregatesFilter<"Court"> | Date | string updatedAt?: Prisma.DateTimeWithAggregatesFilter<"Court"> | Date | string
} }
@@ -321,6 +351,8 @@ export type CourtCreateInput = {
name: string name: string
slotDurationMinutes: number slotDurationMinutes: number
basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string
isUnderMaintenance?: boolean
maintenanceReason?: string | null
createdAt?: Date | string createdAt?: Date | string
updatedAt?: Date | string updatedAt?: Date | string
complex: Prisma.ComplexCreateNestedOneWithoutCourtsInput complex: Prisma.ComplexCreateNestedOneWithoutCourtsInput
@@ -328,6 +360,8 @@ export type CourtCreateInput = {
availabilities?: Prisma.CourtAvailabilityCreateNestedManyWithoutCourtInput availabilities?: Prisma.CourtAvailabilityCreateNestedManyWithoutCourtInput
priceRules?: Prisma.CourtPriceRuleCreateNestedManyWithoutCourtInput priceRules?: Prisma.CourtPriceRuleCreateNestedManyWithoutCourtInput
bookings?: Prisma.CourtBookingCreateNestedManyWithoutCourtInput bookings?: Prisma.CourtBookingCreateNestedManyWithoutCourtInput
maintenances?: Prisma.CourtMaintenanceCreateNestedManyWithoutCourtInput
recurringGroups?: Prisma.RecurringBookingGroupCreateNestedManyWithoutCourtInput
} }
export type CourtUncheckedCreateInput = { export type CourtUncheckedCreateInput = {
@@ -337,11 +371,15 @@ export type CourtUncheckedCreateInput = {
name: string name: string
slotDurationMinutes: number slotDurationMinutes: number
basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string
isUnderMaintenance?: boolean
maintenanceReason?: string | null
createdAt?: Date | string createdAt?: Date | string
updatedAt?: Date | string updatedAt?: Date | string
availabilities?: Prisma.CourtAvailabilityUncheckedCreateNestedManyWithoutCourtInput availabilities?: Prisma.CourtAvailabilityUncheckedCreateNestedManyWithoutCourtInput
priceRules?: Prisma.CourtPriceRuleUncheckedCreateNestedManyWithoutCourtInput priceRules?: Prisma.CourtPriceRuleUncheckedCreateNestedManyWithoutCourtInput
bookings?: Prisma.CourtBookingUncheckedCreateNestedManyWithoutCourtInput bookings?: Prisma.CourtBookingUncheckedCreateNestedManyWithoutCourtInput
maintenances?: Prisma.CourtMaintenanceUncheckedCreateNestedManyWithoutCourtInput
recurringGroups?: Prisma.RecurringBookingGroupUncheckedCreateNestedManyWithoutCourtInput
} }
export type CourtUpdateInput = { export type CourtUpdateInput = {
@@ -349,6 +387,8 @@ export type CourtUpdateInput = {
name?: Prisma.StringFieldUpdateOperationsInput | string name?: Prisma.StringFieldUpdateOperationsInput | string
slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number
basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string
isUnderMaintenance?: Prisma.BoolFieldUpdateOperationsInput | boolean
maintenanceReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
complex?: Prisma.ComplexUpdateOneRequiredWithoutCourtsNestedInput complex?: Prisma.ComplexUpdateOneRequiredWithoutCourtsNestedInput
@@ -356,6 +396,8 @@ export type CourtUpdateInput = {
availabilities?: Prisma.CourtAvailabilityUpdateManyWithoutCourtNestedInput availabilities?: Prisma.CourtAvailabilityUpdateManyWithoutCourtNestedInput
priceRules?: Prisma.CourtPriceRuleUpdateManyWithoutCourtNestedInput priceRules?: Prisma.CourtPriceRuleUpdateManyWithoutCourtNestedInput
bookings?: Prisma.CourtBookingUpdateManyWithoutCourtNestedInput bookings?: Prisma.CourtBookingUpdateManyWithoutCourtNestedInput
maintenances?: Prisma.CourtMaintenanceUpdateManyWithoutCourtNestedInput
recurringGroups?: Prisma.RecurringBookingGroupUpdateManyWithoutCourtNestedInput
} }
export type CourtUncheckedUpdateInput = { export type CourtUncheckedUpdateInput = {
@@ -365,11 +407,15 @@ export type CourtUncheckedUpdateInput = {
name?: Prisma.StringFieldUpdateOperationsInput | string name?: Prisma.StringFieldUpdateOperationsInput | string
slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number
basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string
isUnderMaintenance?: Prisma.BoolFieldUpdateOperationsInput | boolean
maintenanceReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
availabilities?: Prisma.CourtAvailabilityUncheckedUpdateManyWithoutCourtNestedInput availabilities?: Prisma.CourtAvailabilityUncheckedUpdateManyWithoutCourtNestedInput
priceRules?: Prisma.CourtPriceRuleUncheckedUpdateManyWithoutCourtNestedInput priceRules?: Prisma.CourtPriceRuleUncheckedUpdateManyWithoutCourtNestedInput
bookings?: Prisma.CourtBookingUncheckedUpdateManyWithoutCourtNestedInput bookings?: Prisma.CourtBookingUncheckedUpdateManyWithoutCourtNestedInput
maintenances?: Prisma.CourtMaintenanceUncheckedUpdateManyWithoutCourtNestedInput
recurringGroups?: Prisma.RecurringBookingGroupUncheckedUpdateManyWithoutCourtNestedInput
} }
export type CourtCreateManyInput = { export type CourtCreateManyInput = {
@@ -379,6 +425,8 @@ export type CourtCreateManyInput = {
name: string name: string
slotDurationMinutes: number slotDurationMinutes: number
basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string
isUnderMaintenance?: boolean
maintenanceReason?: string | null
createdAt?: Date | string createdAt?: Date | string
updatedAt?: Date | string updatedAt?: Date | string
} }
@@ -388,6 +436,8 @@ export type CourtUpdateManyMutationInput = {
name?: Prisma.StringFieldUpdateOperationsInput | string name?: Prisma.StringFieldUpdateOperationsInput | string
slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number
basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string
isUnderMaintenance?: Prisma.BoolFieldUpdateOperationsInput | boolean
maintenanceReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
} }
@@ -399,6 +449,8 @@ export type CourtUncheckedUpdateManyInput = {
name?: Prisma.StringFieldUpdateOperationsInput | string name?: Prisma.StringFieldUpdateOperationsInput | string
slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number
basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string
isUnderMaintenance?: Prisma.BoolFieldUpdateOperationsInput | boolean
maintenanceReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
} }
@@ -420,6 +472,8 @@ export type CourtCountOrderByAggregateInput = {
name?: Prisma.SortOrder name?: Prisma.SortOrder
slotDurationMinutes?: Prisma.SortOrder slotDurationMinutes?: Prisma.SortOrder
basePrice?: Prisma.SortOrder basePrice?: Prisma.SortOrder
isUnderMaintenance?: Prisma.SortOrder
maintenanceReason?: Prisma.SortOrder
createdAt?: Prisma.SortOrder createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder updatedAt?: Prisma.SortOrder
} }
@@ -436,6 +490,8 @@ export type CourtMaxOrderByAggregateInput = {
name?: Prisma.SortOrder name?: Prisma.SortOrder
slotDurationMinutes?: Prisma.SortOrder slotDurationMinutes?: Prisma.SortOrder
basePrice?: Prisma.SortOrder basePrice?: Prisma.SortOrder
isUnderMaintenance?: Prisma.SortOrder
maintenanceReason?: Prisma.SortOrder
createdAt?: Prisma.SortOrder createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder updatedAt?: Prisma.SortOrder
} }
@@ -447,6 +503,8 @@ export type CourtMinOrderByAggregateInput = {
name?: Prisma.SortOrder name?: Prisma.SortOrder
slotDurationMinutes?: Prisma.SortOrder slotDurationMinutes?: Prisma.SortOrder
basePrice?: Prisma.SortOrder basePrice?: Prisma.SortOrder
isUnderMaintenance?: Prisma.SortOrder
maintenanceReason?: Prisma.SortOrder
createdAt?: Prisma.SortOrder createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder updatedAt?: Prisma.SortOrder
} }
@@ -561,6 +619,20 @@ export type DecimalFieldUpdateOperationsInput = {
divide?: runtime.Decimal | runtime.DecimalJsLike | number | string divide?: runtime.Decimal | runtime.DecimalJsLike | number | string
} }
export type CourtCreateNestedOneWithoutMaintenancesInput = {
create?: Prisma.XOR<Prisma.CourtCreateWithoutMaintenancesInput, Prisma.CourtUncheckedCreateWithoutMaintenancesInput>
connectOrCreate?: Prisma.CourtCreateOrConnectWithoutMaintenancesInput
connect?: Prisma.CourtWhereUniqueInput
}
export type CourtUpdateOneRequiredWithoutMaintenancesNestedInput = {
create?: Prisma.XOR<Prisma.CourtCreateWithoutMaintenancesInput, Prisma.CourtUncheckedCreateWithoutMaintenancesInput>
connectOrCreate?: Prisma.CourtCreateOrConnectWithoutMaintenancesInput
upsert?: Prisma.CourtUpsertWithoutMaintenancesInput
connect?: Prisma.CourtWhereUniqueInput
update?: Prisma.XOR<Prisma.XOR<Prisma.CourtUpdateToOneWithWhereWithoutMaintenancesInput, Prisma.CourtUpdateWithoutMaintenancesInput>, Prisma.CourtUncheckedUpdateWithoutMaintenancesInput>
}
export type CourtCreateNestedOneWithoutAvailabilitiesInput = { export type CourtCreateNestedOneWithoutAvailabilitiesInput = {
create?: Prisma.XOR<Prisma.CourtCreateWithoutAvailabilitiesInput, Prisma.CourtUncheckedCreateWithoutAvailabilitiesInput> create?: Prisma.XOR<Prisma.CourtCreateWithoutAvailabilitiesInput, Prisma.CourtUncheckedCreateWithoutAvailabilitiesInput>
connectOrCreate?: Prisma.CourtCreateOrConnectWithoutAvailabilitiesInput connectOrCreate?: Prisma.CourtCreateOrConnectWithoutAvailabilitiesInput
@@ -603,17 +675,35 @@ 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
slotDurationMinutes: number slotDurationMinutes: number
basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string
isUnderMaintenance?: boolean
maintenanceReason?: string | null
createdAt?: Date | string createdAt?: Date | string
updatedAt?: Date | string updatedAt?: Date | string
sport: Prisma.SportCreateNestedOneWithoutCourtsInput sport: Prisma.SportCreateNestedOneWithoutCourtsInput
availabilities?: Prisma.CourtAvailabilityCreateNestedManyWithoutCourtInput availabilities?: Prisma.CourtAvailabilityCreateNestedManyWithoutCourtInput
priceRules?: Prisma.CourtPriceRuleCreateNestedManyWithoutCourtInput priceRules?: Prisma.CourtPriceRuleCreateNestedManyWithoutCourtInput
bookings?: Prisma.CourtBookingCreateNestedManyWithoutCourtInput bookings?: Prisma.CourtBookingCreateNestedManyWithoutCourtInput
maintenances?: Prisma.CourtMaintenanceCreateNestedManyWithoutCourtInput
recurringGroups?: Prisma.RecurringBookingGroupCreateNestedManyWithoutCourtInput
} }
export type CourtUncheckedCreateWithoutComplexInput = { export type CourtUncheckedCreateWithoutComplexInput = {
@@ -622,11 +712,15 @@ export type CourtUncheckedCreateWithoutComplexInput = {
name: string name: string
slotDurationMinutes: number slotDurationMinutes: number
basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string
isUnderMaintenance?: boolean
maintenanceReason?: string | null
createdAt?: Date | string createdAt?: Date | string
updatedAt?: Date | string updatedAt?: Date | string
availabilities?: Prisma.CourtAvailabilityUncheckedCreateNestedManyWithoutCourtInput availabilities?: Prisma.CourtAvailabilityUncheckedCreateNestedManyWithoutCourtInput
priceRules?: Prisma.CourtPriceRuleUncheckedCreateNestedManyWithoutCourtInput priceRules?: Prisma.CourtPriceRuleUncheckedCreateNestedManyWithoutCourtInput
bookings?: Prisma.CourtBookingUncheckedCreateNestedManyWithoutCourtInput bookings?: Prisma.CourtBookingUncheckedCreateNestedManyWithoutCourtInput
maintenances?: Prisma.CourtMaintenanceUncheckedCreateNestedManyWithoutCourtInput
recurringGroups?: Prisma.RecurringBookingGroupUncheckedCreateNestedManyWithoutCourtInput
} }
export type CourtCreateOrConnectWithoutComplexInput = { export type CourtCreateOrConnectWithoutComplexInput = {
@@ -665,6 +759,8 @@ export type CourtScalarWhereInput = {
name?: Prisma.StringFilter<"Court"> | string name?: Prisma.StringFilter<"Court"> | string
slotDurationMinutes?: Prisma.IntFilter<"Court"> | number slotDurationMinutes?: Prisma.IntFilter<"Court"> | number
basePrice?: Prisma.DecimalFilter<"Court"> | runtime.Decimal | runtime.DecimalJsLike | number | string basePrice?: Prisma.DecimalFilter<"Court"> | runtime.Decimal | runtime.DecimalJsLike | number | string
isUnderMaintenance?: Prisma.BoolFilter<"Court"> | boolean
maintenanceReason?: Prisma.StringNullableFilter<"Court"> | string | null
createdAt?: Prisma.DateTimeFilter<"Court"> | Date | string createdAt?: Prisma.DateTimeFilter<"Court"> | Date | string
updatedAt?: Prisma.DateTimeFilter<"Court"> | Date | string updatedAt?: Prisma.DateTimeFilter<"Court"> | Date | string
} }
@@ -674,12 +770,16 @@ export type CourtCreateWithoutSportInput = {
name: string name: string
slotDurationMinutes: number slotDurationMinutes: number
basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string
isUnderMaintenance?: boolean
maintenanceReason?: string | null
createdAt?: Date | string createdAt?: Date | string
updatedAt?: Date | string updatedAt?: Date | string
complex: Prisma.ComplexCreateNestedOneWithoutCourtsInput complex: Prisma.ComplexCreateNestedOneWithoutCourtsInput
availabilities?: Prisma.CourtAvailabilityCreateNestedManyWithoutCourtInput availabilities?: Prisma.CourtAvailabilityCreateNestedManyWithoutCourtInput
priceRules?: Prisma.CourtPriceRuleCreateNestedManyWithoutCourtInput priceRules?: Prisma.CourtPriceRuleCreateNestedManyWithoutCourtInput
bookings?: Prisma.CourtBookingCreateNestedManyWithoutCourtInput bookings?: Prisma.CourtBookingCreateNestedManyWithoutCourtInput
maintenances?: Prisma.CourtMaintenanceCreateNestedManyWithoutCourtInput
recurringGroups?: Prisma.RecurringBookingGroupCreateNestedManyWithoutCourtInput
} }
export type CourtUncheckedCreateWithoutSportInput = { export type CourtUncheckedCreateWithoutSportInput = {
@@ -688,11 +788,15 @@ export type CourtUncheckedCreateWithoutSportInput = {
name: string name: string
slotDurationMinutes: number slotDurationMinutes: number
basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string
isUnderMaintenance?: boolean
maintenanceReason?: string | null
createdAt?: Date | string createdAt?: Date | string
updatedAt?: Date | string updatedAt?: Date | string
availabilities?: Prisma.CourtAvailabilityUncheckedCreateNestedManyWithoutCourtInput availabilities?: Prisma.CourtAvailabilityUncheckedCreateNestedManyWithoutCourtInput
priceRules?: Prisma.CourtPriceRuleUncheckedCreateNestedManyWithoutCourtInput priceRules?: Prisma.CourtPriceRuleUncheckedCreateNestedManyWithoutCourtInput
bookings?: Prisma.CourtBookingUncheckedCreateNestedManyWithoutCourtInput bookings?: Prisma.CourtBookingUncheckedCreateNestedManyWithoutCourtInput
maintenances?: Prisma.CourtMaintenanceUncheckedCreateNestedManyWithoutCourtInput
recurringGroups?: Prisma.RecurringBookingGroupUncheckedCreateNestedManyWithoutCourtInput
} }
export type CourtCreateOrConnectWithoutSportInput = { export type CourtCreateOrConnectWithoutSportInput = {
@@ -721,17 +825,105 @@ export type CourtUpdateManyWithWhereWithoutSportInput = {
data: Prisma.XOR<Prisma.CourtUpdateManyMutationInput, Prisma.CourtUncheckedUpdateManyWithoutSportInput> data: Prisma.XOR<Prisma.CourtUpdateManyMutationInput, Prisma.CourtUncheckedUpdateManyWithoutSportInput>
} }
export type CourtCreateWithoutMaintenancesInput = {
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
recurringGroups?: Prisma.RecurringBookingGroupCreateNestedManyWithoutCourtInput
}
export type CourtUncheckedCreateWithoutMaintenancesInput = {
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
recurringGroups?: Prisma.RecurringBookingGroupUncheckedCreateNestedManyWithoutCourtInput
}
export type CourtCreateOrConnectWithoutMaintenancesInput = {
where: Prisma.CourtWhereUniqueInput
create: Prisma.XOR<Prisma.CourtCreateWithoutMaintenancesInput, Prisma.CourtUncheckedCreateWithoutMaintenancesInput>
}
export type CourtUpsertWithoutMaintenancesInput = {
update: Prisma.XOR<Prisma.CourtUpdateWithoutMaintenancesInput, Prisma.CourtUncheckedUpdateWithoutMaintenancesInput>
create: Prisma.XOR<Prisma.CourtCreateWithoutMaintenancesInput, Prisma.CourtUncheckedCreateWithoutMaintenancesInput>
where?: Prisma.CourtWhereInput
}
export type CourtUpdateToOneWithWhereWithoutMaintenancesInput = {
where?: Prisma.CourtWhereInput
data: Prisma.XOR<Prisma.CourtUpdateWithoutMaintenancesInput, Prisma.CourtUncheckedUpdateWithoutMaintenancesInput>
}
export type CourtUpdateWithoutMaintenancesInput = {
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
recurringGroups?: Prisma.RecurringBookingGroupUpdateManyWithoutCourtNestedInput
}
export type CourtUncheckedUpdateWithoutMaintenancesInput = {
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
recurringGroups?: Prisma.RecurringBookingGroupUncheckedUpdateManyWithoutCourtNestedInput
}
export type CourtCreateWithoutAvailabilitiesInput = { export type CourtCreateWithoutAvailabilitiesInput = {
id: string id: string
name: string name: string
slotDurationMinutes: number slotDurationMinutes: number
basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string
isUnderMaintenance?: boolean
maintenanceReason?: string | null
createdAt?: Date | string createdAt?: Date | string
updatedAt?: Date | string updatedAt?: Date | string
complex: Prisma.ComplexCreateNestedOneWithoutCourtsInput complex: Prisma.ComplexCreateNestedOneWithoutCourtsInput
sport: Prisma.SportCreateNestedOneWithoutCourtsInput sport: Prisma.SportCreateNestedOneWithoutCourtsInput
priceRules?: Prisma.CourtPriceRuleCreateNestedManyWithoutCourtInput priceRules?: Prisma.CourtPriceRuleCreateNestedManyWithoutCourtInput
bookings?: Prisma.CourtBookingCreateNestedManyWithoutCourtInput bookings?: Prisma.CourtBookingCreateNestedManyWithoutCourtInput
maintenances?: Prisma.CourtMaintenanceCreateNestedManyWithoutCourtInput
recurringGroups?: Prisma.RecurringBookingGroupCreateNestedManyWithoutCourtInput
} }
export type CourtUncheckedCreateWithoutAvailabilitiesInput = { export type CourtUncheckedCreateWithoutAvailabilitiesInput = {
@@ -741,10 +933,14 @@ export type CourtUncheckedCreateWithoutAvailabilitiesInput = {
name: string name: string
slotDurationMinutes: number slotDurationMinutes: number
basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string
isUnderMaintenance?: boolean
maintenanceReason?: string | null
createdAt?: Date | string createdAt?: Date | string
updatedAt?: Date | string updatedAt?: Date | string
priceRules?: Prisma.CourtPriceRuleUncheckedCreateNestedManyWithoutCourtInput priceRules?: Prisma.CourtPriceRuleUncheckedCreateNestedManyWithoutCourtInput
bookings?: Prisma.CourtBookingUncheckedCreateNestedManyWithoutCourtInput bookings?: Prisma.CourtBookingUncheckedCreateNestedManyWithoutCourtInput
maintenances?: Prisma.CourtMaintenanceUncheckedCreateNestedManyWithoutCourtInput
recurringGroups?: Prisma.RecurringBookingGroupUncheckedCreateNestedManyWithoutCourtInput
} }
export type CourtCreateOrConnectWithoutAvailabilitiesInput = { export type CourtCreateOrConnectWithoutAvailabilitiesInput = {
@@ -768,12 +964,16 @@ export type CourtUpdateWithoutAvailabilitiesInput = {
name?: Prisma.StringFieldUpdateOperationsInput | string name?: Prisma.StringFieldUpdateOperationsInput | string
slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number
basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string
isUnderMaintenance?: Prisma.BoolFieldUpdateOperationsInput | boolean
maintenanceReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
complex?: Prisma.ComplexUpdateOneRequiredWithoutCourtsNestedInput complex?: Prisma.ComplexUpdateOneRequiredWithoutCourtsNestedInput
sport?: Prisma.SportUpdateOneRequiredWithoutCourtsNestedInput sport?: Prisma.SportUpdateOneRequiredWithoutCourtsNestedInput
priceRules?: Prisma.CourtPriceRuleUpdateManyWithoutCourtNestedInput priceRules?: Prisma.CourtPriceRuleUpdateManyWithoutCourtNestedInput
bookings?: Prisma.CourtBookingUpdateManyWithoutCourtNestedInput bookings?: Prisma.CourtBookingUpdateManyWithoutCourtNestedInput
maintenances?: Prisma.CourtMaintenanceUpdateManyWithoutCourtNestedInput
recurringGroups?: Prisma.RecurringBookingGroupUpdateManyWithoutCourtNestedInput
} }
export type CourtUncheckedUpdateWithoutAvailabilitiesInput = { export type CourtUncheckedUpdateWithoutAvailabilitiesInput = {
@@ -783,10 +983,14 @@ export type CourtUncheckedUpdateWithoutAvailabilitiesInput = {
name?: Prisma.StringFieldUpdateOperationsInput | string name?: Prisma.StringFieldUpdateOperationsInput | string
slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number
basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string
isUnderMaintenance?: Prisma.BoolFieldUpdateOperationsInput | boolean
maintenanceReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
priceRules?: Prisma.CourtPriceRuleUncheckedUpdateManyWithoutCourtNestedInput priceRules?: Prisma.CourtPriceRuleUncheckedUpdateManyWithoutCourtNestedInput
bookings?: Prisma.CourtBookingUncheckedUpdateManyWithoutCourtNestedInput bookings?: Prisma.CourtBookingUncheckedUpdateManyWithoutCourtNestedInput
maintenances?: Prisma.CourtMaintenanceUncheckedUpdateManyWithoutCourtNestedInput
recurringGroups?: Prisma.RecurringBookingGroupUncheckedUpdateManyWithoutCourtNestedInput
} }
export type CourtCreateWithoutPriceRulesInput = { export type CourtCreateWithoutPriceRulesInput = {
@@ -794,12 +998,16 @@ export type CourtCreateWithoutPriceRulesInput = {
name: string name: string
slotDurationMinutes: number slotDurationMinutes: number
basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string
isUnderMaintenance?: boolean
maintenanceReason?: string | null
createdAt?: Date | string createdAt?: Date | string
updatedAt?: Date | string updatedAt?: Date | string
complex: Prisma.ComplexCreateNestedOneWithoutCourtsInput complex: Prisma.ComplexCreateNestedOneWithoutCourtsInput
sport: Prisma.SportCreateNestedOneWithoutCourtsInput sport: Prisma.SportCreateNestedOneWithoutCourtsInput
availabilities?: Prisma.CourtAvailabilityCreateNestedManyWithoutCourtInput availabilities?: Prisma.CourtAvailabilityCreateNestedManyWithoutCourtInput
bookings?: Prisma.CourtBookingCreateNestedManyWithoutCourtInput bookings?: Prisma.CourtBookingCreateNestedManyWithoutCourtInput
maintenances?: Prisma.CourtMaintenanceCreateNestedManyWithoutCourtInput
recurringGroups?: Prisma.RecurringBookingGroupCreateNestedManyWithoutCourtInput
} }
export type CourtUncheckedCreateWithoutPriceRulesInput = { export type CourtUncheckedCreateWithoutPriceRulesInput = {
@@ -809,10 +1017,14 @@ export type CourtUncheckedCreateWithoutPriceRulesInput = {
name: string name: string
slotDurationMinutes: number slotDurationMinutes: number
basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string
isUnderMaintenance?: boolean
maintenanceReason?: string | null
createdAt?: Date | string createdAt?: Date | string
updatedAt?: Date | string updatedAt?: Date | string
availabilities?: Prisma.CourtAvailabilityUncheckedCreateNestedManyWithoutCourtInput availabilities?: Prisma.CourtAvailabilityUncheckedCreateNestedManyWithoutCourtInput
bookings?: Prisma.CourtBookingUncheckedCreateNestedManyWithoutCourtInput bookings?: Prisma.CourtBookingUncheckedCreateNestedManyWithoutCourtInput
maintenances?: Prisma.CourtMaintenanceUncheckedCreateNestedManyWithoutCourtInput
recurringGroups?: Prisma.RecurringBookingGroupUncheckedCreateNestedManyWithoutCourtInput
} }
export type CourtCreateOrConnectWithoutPriceRulesInput = { export type CourtCreateOrConnectWithoutPriceRulesInput = {
@@ -836,12 +1048,16 @@ export type CourtUpdateWithoutPriceRulesInput = {
name?: Prisma.StringFieldUpdateOperationsInput | string name?: Prisma.StringFieldUpdateOperationsInput | string
slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number
basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string
isUnderMaintenance?: Prisma.BoolFieldUpdateOperationsInput | boolean
maintenanceReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
complex?: Prisma.ComplexUpdateOneRequiredWithoutCourtsNestedInput complex?: Prisma.ComplexUpdateOneRequiredWithoutCourtsNestedInput
sport?: Prisma.SportUpdateOneRequiredWithoutCourtsNestedInput sport?: Prisma.SportUpdateOneRequiredWithoutCourtsNestedInput
availabilities?: Prisma.CourtAvailabilityUpdateManyWithoutCourtNestedInput availabilities?: Prisma.CourtAvailabilityUpdateManyWithoutCourtNestedInput
bookings?: Prisma.CourtBookingUpdateManyWithoutCourtNestedInput bookings?: Prisma.CourtBookingUpdateManyWithoutCourtNestedInput
maintenances?: Prisma.CourtMaintenanceUpdateManyWithoutCourtNestedInput
recurringGroups?: Prisma.RecurringBookingGroupUpdateManyWithoutCourtNestedInput
} }
export type CourtUncheckedUpdateWithoutPriceRulesInput = { export type CourtUncheckedUpdateWithoutPriceRulesInput = {
@@ -851,10 +1067,14 @@ export type CourtUncheckedUpdateWithoutPriceRulesInput = {
name?: Prisma.StringFieldUpdateOperationsInput | string name?: Prisma.StringFieldUpdateOperationsInput | string
slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number
basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string
isUnderMaintenance?: Prisma.BoolFieldUpdateOperationsInput | boolean
maintenanceReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
availabilities?: Prisma.CourtAvailabilityUncheckedUpdateManyWithoutCourtNestedInput availabilities?: Prisma.CourtAvailabilityUncheckedUpdateManyWithoutCourtNestedInput
bookings?: Prisma.CourtBookingUncheckedUpdateManyWithoutCourtNestedInput bookings?: Prisma.CourtBookingUncheckedUpdateManyWithoutCourtNestedInput
maintenances?: Prisma.CourtMaintenanceUncheckedUpdateManyWithoutCourtNestedInput
recurringGroups?: Prisma.RecurringBookingGroupUncheckedUpdateManyWithoutCourtNestedInput
} }
export type CourtCreateWithoutBookingsInput = { export type CourtCreateWithoutBookingsInput = {
@@ -862,12 +1082,16 @@ export type CourtCreateWithoutBookingsInput = {
name: string name: string
slotDurationMinutes: number slotDurationMinutes: number
basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string
isUnderMaintenance?: boolean
maintenanceReason?: string | null
createdAt?: Date | string createdAt?: Date | string
updatedAt?: Date | string updatedAt?: Date | string
complex: Prisma.ComplexCreateNestedOneWithoutCourtsInput complex: Prisma.ComplexCreateNestedOneWithoutCourtsInput
sport: Prisma.SportCreateNestedOneWithoutCourtsInput sport: Prisma.SportCreateNestedOneWithoutCourtsInput
availabilities?: Prisma.CourtAvailabilityCreateNestedManyWithoutCourtInput availabilities?: Prisma.CourtAvailabilityCreateNestedManyWithoutCourtInput
priceRules?: Prisma.CourtPriceRuleCreateNestedManyWithoutCourtInput priceRules?: Prisma.CourtPriceRuleCreateNestedManyWithoutCourtInput
maintenances?: Prisma.CourtMaintenanceCreateNestedManyWithoutCourtInput
recurringGroups?: Prisma.RecurringBookingGroupCreateNestedManyWithoutCourtInput
} }
export type CourtUncheckedCreateWithoutBookingsInput = { export type CourtUncheckedCreateWithoutBookingsInput = {
@@ -877,10 +1101,14 @@ export type CourtUncheckedCreateWithoutBookingsInput = {
name: string name: string
slotDurationMinutes: number slotDurationMinutes: number
basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string
isUnderMaintenance?: boolean
maintenanceReason?: string | null
createdAt?: Date | string createdAt?: Date | string
updatedAt?: Date | string updatedAt?: Date | string
availabilities?: Prisma.CourtAvailabilityUncheckedCreateNestedManyWithoutCourtInput availabilities?: Prisma.CourtAvailabilityUncheckedCreateNestedManyWithoutCourtInput
priceRules?: Prisma.CourtPriceRuleUncheckedCreateNestedManyWithoutCourtInput priceRules?: Prisma.CourtPriceRuleUncheckedCreateNestedManyWithoutCourtInput
maintenances?: Prisma.CourtMaintenanceUncheckedCreateNestedManyWithoutCourtInput
recurringGroups?: Prisma.RecurringBookingGroupUncheckedCreateNestedManyWithoutCourtInput
} }
export type CourtCreateOrConnectWithoutBookingsInput = { export type CourtCreateOrConnectWithoutBookingsInput = {
@@ -904,12 +1132,16 @@ export type CourtUpdateWithoutBookingsInput = {
name?: Prisma.StringFieldUpdateOperationsInput | string name?: Prisma.StringFieldUpdateOperationsInput | string
slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number
basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string
isUnderMaintenance?: Prisma.BoolFieldUpdateOperationsInput | boolean
maintenanceReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
complex?: Prisma.ComplexUpdateOneRequiredWithoutCourtsNestedInput complex?: Prisma.ComplexUpdateOneRequiredWithoutCourtsNestedInput
sport?: Prisma.SportUpdateOneRequiredWithoutCourtsNestedInput sport?: Prisma.SportUpdateOneRequiredWithoutCourtsNestedInput
availabilities?: Prisma.CourtAvailabilityUpdateManyWithoutCourtNestedInput availabilities?: Prisma.CourtAvailabilityUpdateManyWithoutCourtNestedInput
priceRules?: Prisma.CourtPriceRuleUpdateManyWithoutCourtNestedInput priceRules?: Prisma.CourtPriceRuleUpdateManyWithoutCourtNestedInput
maintenances?: Prisma.CourtMaintenanceUpdateManyWithoutCourtNestedInput
recurringGroups?: Prisma.RecurringBookingGroupUpdateManyWithoutCourtNestedInput
} }
export type CourtUncheckedUpdateWithoutBookingsInput = { export type CourtUncheckedUpdateWithoutBookingsInput = {
@@ -919,10 +1151,98 @@ export type CourtUncheckedUpdateWithoutBookingsInput = {
name?: Prisma.StringFieldUpdateOperationsInput | string name?: Prisma.StringFieldUpdateOperationsInput | string
slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number
basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string
isUnderMaintenance?: Prisma.BoolFieldUpdateOperationsInput | boolean
maintenanceReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
availabilities?: Prisma.CourtAvailabilityUncheckedUpdateManyWithoutCourtNestedInput availabilities?: Prisma.CourtAvailabilityUncheckedUpdateManyWithoutCourtNestedInput
priceRules?: Prisma.CourtPriceRuleUncheckedUpdateManyWithoutCourtNestedInput priceRules?: Prisma.CourtPriceRuleUncheckedUpdateManyWithoutCourtNestedInput
maintenances?: Prisma.CourtMaintenanceUncheckedUpdateManyWithoutCourtNestedInput
recurringGroups?: Prisma.RecurringBookingGroupUncheckedUpdateManyWithoutCourtNestedInput
}
export type CourtCreateWithoutRecurringGroupsInput = {
id: string
name: string
slotDurationMinutes: number
basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string
isUnderMaintenance?: boolean
maintenanceReason?: string | null
createdAt?: Date | string
updatedAt?: Date | string
complex: Prisma.ComplexCreateNestedOneWithoutCourtsInput
sport: Prisma.SportCreateNestedOneWithoutCourtsInput
availabilities?: Prisma.CourtAvailabilityCreateNestedManyWithoutCourtInput
priceRules?: Prisma.CourtPriceRuleCreateNestedManyWithoutCourtInput
bookings?: Prisma.CourtBookingCreateNestedManyWithoutCourtInput
maintenances?: Prisma.CourtMaintenanceCreateNestedManyWithoutCourtInput
}
export type CourtUncheckedCreateWithoutRecurringGroupsInput = {
id: string
complexId: string
sportId: string
name: string
slotDurationMinutes: number
basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string
isUnderMaintenance?: boolean
maintenanceReason?: string | null
createdAt?: Date | string
updatedAt?: Date | string
availabilities?: Prisma.CourtAvailabilityUncheckedCreateNestedManyWithoutCourtInput
priceRules?: Prisma.CourtPriceRuleUncheckedCreateNestedManyWithoutCourtInput
bookings?: Prisma.CourtBookingUncheckedCreateNestedManyWithoutCourtInput
maintenances?: Prisma.CourtMaintenanceUncheckedCreateNestedManyWithoutCourtInput
}
export type CourtCreateOrConnectWithoutRecurringGroupsInput = {
where: Prisma.CourtWhereUniqueInput
create: Prisma.XOR<Prisma.CourtCreateWithoutRecurringGroupsInput, Prisma.CourtUncheckedCreateWithoutRecurringGroupsInput>
}
export type CourtUpsertWithoutRecurringGroupsInput = {
update: Prisma.XOR<Prisma.CourtUpdateWithoutRecurringGroupsInput, Prisma.CourtUncheckedUpdateWithoutRecurringGroupsInput>
create: Prisma.XOR<Prisma.CourtCreateWithoutRecurringGroupsInput, Prisma.CourtUncheckedCreateWithoutRecurringGroupsInput>
where?: Prisma.CourtWhereInput
}
export type CourtUpdateToOneWithWhereWithoutRecurringGroupsInput = {
where?: Prisma.CourtWhereInput
data: Prisma.XOR<Prisma.CourtUpdateWithoutRecurringGroupsInput, Prisma.CourtUncheckedUpdateWithoutRecurringGroupsInput>
}
export type CourtUpdateWithoutRecurringGroupsInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number
basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string
isUnderMaintenance?: Prisma.BoolFieldUpdateOperationsInput | boolean
maintenanceReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
complex?: Prisma.ComplexUpdateOneRequiredWithoutCourtsNestedInput
sport?: Prisma.SportUpdateOneRequiredWithoutCourtsNestedInput
availabilities?: Prisma.CourtAvailabilityUpdateManyWithoutCourtNestedInput
priceRules?: Prisma.CourtPriceRuleUpdateManyWithoutCourtNestedInput
bookings?: Prisma.CourtBookingUpdateManyWithoutCourtNestedInput
maintenances?: Prisma.CourtMaintenanceUpdateManyWithoutCourtNestedInput
}
export type CourtUncheckedUpdateWithoutRecurringGroupsInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
complexId?: Prisma.StringFieldUpdateOperationsInput | string
sportId?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number
basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string
isUnderMaintenance?: Prisma.BoolFieldUpdateOperationsInput | boolean
maintenanceReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
availabilities?: Prisma.CourtAvailabilityUncheckedUpdateManyWithoutCourtNestedInput
priceRules?: Prisma.CourtPriceRuleUncheckedUpdateManyWithoutCourtNestedInput
bookings?: Prisma.CourtBookingUncheckedUpdateManyWithoutCourtNestedInput
maintenances?: Prisma.CourtMaintenanceUncheckedUpdateManyWithoutCourtNestedInput
} }
export type CourtCreateManyComplexInput = { export type CourtCreateManyComplexInput = {
@@ -931,6 +1251,8 @@ export type CourtCreateManyComplexInput = {
name: string name: string
slotDurationMinutes: number slotDurationMinutes: number
basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string
isUnderMaintenance?: boolean
maintenanceReason?: string | null
createdAt?: Date | string createdAt?: Date | string
updatedAt?: Date | string updatedAt?: Date | string
} }
@@ -940,12 +1262,16 @@ export type CourtUpdateWithoutComplexInput = {
name?: Prisma.StringFieldUpdateOperationsInput | string name?: Prisma.StringFieldUpdateOperationsInput | string
slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number
basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string
isUnderMaintenance?: Prisma.BoolFieldUpdateOperationsInput | boolean
maintenanceReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
sport?: Prisma.SportUpdateOneRequiredWithoutCourtsNestedInput sport?: Prisma.SportUpdateOneRequiredWithoutCourtsNestedInput
availabilities?: Prisma.CourtAvailabilityUpdateManyWithoutCourtNestedInput availabilities?: Prisma.CourtAvailabilityUpdateManyWithoutCourtNestedInput
priceRules?: Prisma.CourtPriceRuleUpdateManyWithoutCourtNestedInput priceRules?: Prisma.CourtPriceRuleUpdateManyWithoutCourtNestedInput
bookings?: Prisma.CourtBookingUpdateManyWithoutCourtNestedInput bookings?: Prisma.CourtBookingUpdateManyWithoutCourtNestedInput
maintenances?: Prisma.CourtMaintenanceUpdateManyWithoutCourtNestedInput
recurringGroups?: Prisma.RecurringBookingGroupUpdateManyWithoutCourtNestedInput
} }
export type CourtUncheckedUpdateWithoutComplexInput = { export type CourtUncheckedUpdateWithoutComplexInput = {
@@ -954,11 +1280,15 @@ export type CourtUncheckedUpdateWithoutComplexInput = {
name?: Prisma.StringFieldUpdateOperationsInput | string name?: Prisma.StringFieldUpdateOperationsInput | string
slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number
basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string
isUnderMaintenance?: Prisma.BoolFieldUpdateOperationsInput | boolean
maintenanceReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
availabilities?: Prisma.CourtAvailabilityUncheckedUpdateManyWithoutCourtNestedInput availabilities?: Prisma.CourtAvailabilityUncheckedUpdateManyWithoutCourtNestedInput
priceRules?: Prisma.CourtPriceRuleUncheckedUpdateManyWithoutCourtNestedInput priceRules?: Prisma.CourtPriceRuleUncheckedUpdateManyWithoutCourtNestedInput
bookings?: Prisma.CourtBookingUncheckedUpdateManyWithoutCourtNestedInput bookings?: Prisma.CourtBookingUncheckedUpdateManyWithoutCourtNestedInput
maintenances?: Prisma.CourtMaintenanceUncheckedUpdateManyWithoutCourtNestedInput
recurringGroups?: Prisma.RecurringBookingGroupUncheckedUpdateManyWithoutCourtNestedInput
} }
export type CourtUncheckedUpdateManyWithoutComplexInput = { export type CourtUncheckedUpdateManyWithoutComplexInput = {
@@ -967,6 +1297,8 @@ export type CourtUncheckedUpdateManyWithoutComplexInput = {
name?: Prisma.StringFieldUpdateOperationsInput | string name?: Prisma.StringFieldUpdateOperationsInput | string
slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number
basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string
isUnderMaintenance?: Prisma.BoolFieldUpdateOperationsInput | boolean
maintenanceReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
} }
@@ -977,6 +1309,8 @@ export type CourtCreateManySportInput = {
name: string name: string
slotDurationMinutes: number slotDurationMinutes: number
basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string
isUnderMaintenance?: boolean
maintenanceReason?: string | null
createdAt?: Date | string createdAt?: Date | string
updatedAt?: Date | string updatedAt?: Date | string
} }
@@ -986,12 +1320,16 @@ export type CourtUpdateWithoutSportInput = {
name?: Prisma.StringFieldUpdateOperationsInput | string name?: Prisma.StringFieldUpdateOperationsInput | string
slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number
basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string
isUnderMaintenance?: Prisma.BoolFieldUpdateOperationsInput | boolean
maintenanceReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
complex?: Prisma.ComplexUpdateOneRequiredWithoutCourtsNestedInput complex?: Prisma.ComplexUpdateOneRequiredWithoutCourtsNestedInput
availabilities?: Prisma.CourtAvailabilityUpdateManyWithoutCourtNestedInput availabilities?: Prisma.CourtAvailabilityUpdateManyWithoutCourtNestedInput
priceRules?: Prisma.CourtPriceRuleUpdateManyWithoutCourtNestedInput priceRules?: Prisma.CourtPriceRuleUpdateManyWithoutCourtNestedInput
bookings?: Prisma.CourtBookingUpdateManyWithoutCourtNestedInput bookings?: Prisma.CourtBookingUpdateManyWithoutCourtNestedInput
maintenances?: Prisma.CourtMaintenanceUpdateManyWithoutCourtNestedInput
recurringGroups?: Prisma.RecurringBookingGroupUpdateManyWithoutCourtNestedInput
} }
export type CourtUncheckedUpdateWithoutSportInput = { export type CourtUncheckedUpdateWithoutSportInput = {
@@ -1000,11 +1338,15 @@ export type CourtUncheckedUpdateWithoutSportInput = {
name?: Prisma.StringFieldUpdateOperationsInput | string name?: Prisma.StringFieldUpdateOperationsInput | string
slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number
basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string
isUnderMaintenance?: Prisma.BoolFieldUpdateOperationsInput | boolean
maintenanceReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
availabilities?: Prisma.CourtAvailabilityUncheckedUpdateManyWithoutCourtNestedInput availabilities?: Prisma.CourtAvailabilityUncheckedUpdateManyWithoutCourtNestedInput
priceRules?: Prisma.CourtPriceRuleUncheckedUpdateManyWithoutCourtNestedInput priceRules?: Prisma.CourtPriceRuleUncheckedUpdateManyWithoutCourtNestedInput
bookings?: Prisma.CourtBookingUncheckedUpdateManyWithoutCourtNestedInput bookings?: Prisma.CourtBookingUncheckedUpdateManyWithoutCourtNestedInput
maintenances?: Prisma.CourtMaintenanceUncheckedUpdateManyWithoutCourtNestedInput
recurringGroups?: Prisma.RecurringBookingGroupUncheckedUpdateManyWithoutCourtNestedInput
} }
export type CourtUncheckedUpdateManyWithoutSportInput = { export type CourtUncheckedUpdateManyWithoutSportInput = {
@@ -1013,6 +1355,8 @@ export type CourtUncheckedUpdateManyWithoutSportInput = {
name?: Prisma.StringFieldUpdateOperationsInput | string name?: Prisma.StringFieldUpdateOperationsInput | string
slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number
basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string
isUnderMaintenance?: Prisma.BoolFieldUpdateOperationsInput | boolean
maintenanceReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
} }
@@ -1026,12 +1370,16 @@ export type CourtCountOutputType = {
availabilities: number availabilities: number
priceRules: number priceRules: number
bookings: number bookings: 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> = {
availabilities?: boolean | CourtCountOutputTypeCountAvailabilitiesArgs availabilities?: boolean | CourtCountOutputTypeCountAvailabilitiesArgs
priceRules?: boolean | CourtCountOutputTypeCountPriceRulesArgs priceRules?: boolean | CourtCountOutputTypeCountPriceRulesArgs
bookings?: boolean | CourtCountOutputTypeCountBookingsArgs bookings?: boolean | CourtCountOutputTypeCountBookingsArgs
maintenances?: boolean | CourtCountOutputTypeCountMaintenancesArgs
recurringGroups?: boolean | CourtCountOutputTypeCountRecurringGroupsArgs
} }
/** /**
@@ -1065,6 +1413,20 @@ export type CourtCountOutputTypeCountBookingsArgs<ExtArgs extends runtime.Types.
where?: Prisma.CourtBookingWhereInput where?: Prisma.CourtBookingWhereInput
} }
/**
* CourtCountOutputType without action
*/
export type CourtCountOutputTypeCountMaintenancesArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
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
@@ -1073,6 +1435,8 @@ export type CourtSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs =
name?: boolean name?: boolean
slotDurationMinutes?: boolean slotDurationMinutes?: boolean
basePrice?: boolean basePrice?: boolean
isUnderMaintenance?: boolean
maintenanceReason?: boolean
createdAt?: boolean createdAt?: boolean
updatedAt?: boolean updatedAt?: boolean
complex?: boolean | Prisma.ComplexDefaultArgs<ExtArgs> complex?: boolean | Prisma.ComplexDefaultArgs<ExtArgs>
@@ -1080,6 +1444,8 @@ export type CourtSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs =
availabilities?: boolean | Prisma.Court$availabilitiesArgs<ExtArgs> availabilities?: boolean | Prisma.Court$availabilitiesArgs<ExtArgs>
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>
recurringGroups?: boolean | Prisma.Court$recurringGroupsArgs<ExtArgs>
_count?: boolean | Prisma.CourtCountOutputTypeDefaultArgs<ExtArgs> _count?: boolean | Prisma.CourtCountOutputTypeDefaultArgs<ExtArgs>
}, ExtArgs["result"]["court"]> }, ExtArgs["result"]["court"]>
@@ -1090,6 +1456,8 @@ export type CourtSelectCreateManyAndReturn<ExtArgs extends runtime.Types.Extensi
name?: boolean name?: boolean
slotDurationMinutes?: boolean slotDurationMinutes?: boolean
basePrice?: boolean basePrice?: boolean
isUnderMaintenance?: boolean
maintenanceReason?: boolean
createdAt?: boolean createdAt?: boolean
updatedAt?: boolean updatedAt?: boolean
complex?: boolean | Prisma.ComplexDefaultArgs<ExtArgs> complex?: boolean | Prisma.ComplexDefaultArgs<ExtArgs>
@@ -1103,6 +1471,8 @@ export type CourtSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensi
name?: boolean name?: boolean
slotDurationMinutes?: boolean slotDurationMinutes?: boolean
basePrice?: boolean basePrice?: boolean
isUnderMaintenance?: boolean
maintenanceReason?: boolean
createdAt?: boolean createdAt?: boolean
updatedAt?: boolean updatedAt?: boolean
complex?: boolean | Prisma.ComplexDefaultArgs<ExtArgs> complex?: boolean | Prisma.ComplexDefaultArgs<ExtArgs>
@@ -1116,17 +1486,21 @@ export type CourtSelectScalar = {
name?: boolean name?: boolean
slotDurationMinutes?: boolean slotDurationMinutes?: boolean
basePrice?: boolean basePrice?: boolean
isUnderMaintenance?: boolean
maintenanceReason?: boolean
createdAt?: boolean createdAt?: boolean
updatedAt?: boolean updatedAt?: boolean
} }
export type CourtOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "complexId" | "sportId" | "name" | "slotDurationMinutes" | "basePrice" | "createdAt" | "updatedAt", ExtArgs["result"]["court"]> export type CourtOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "complexId" | "sportId" | "name" | "slotDurationMinutes" | "basePrice" | "isUnderMaintenance" | "maintenanceReason" | "createdAt" | "updatedAt", ExtArgs["result"]["court"]>
export type CourtInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = { export type CourtInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
complex?: boolean | Prisma.ComplexDefaultArgs<ExtArgs> complex?: boolean | Prisma.ComplexDefaultArgs<ExtArgs>
sport?: boolean | Prisma.SportDefaultArgs<ExtArgs> sport?: boolean | Prisma.SportDefaultArgs<ExtArgs>
availabilities?: boolean | Prisma.Court$availabilitiesArgs<ExtArgs> availabilities?: boolean | Prisma.Court$availabilitiesArgs<ExtArgs>
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>
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> = {
@@ -1146,6 +1520,8 @@ export type $CourtPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs
availabilities: Prisma.$CourtAvailabilityPayload<ExtArgs>[] availabilities: Prisma.$CourtAvailabilityPayload<ExtArgs>[]
priceRules: Prisma.$CourtPriceRulePayload<ExtArgs>[] priceRules: Prisma.$CourtPriceRulePayload<ExtArgs>[]
bookings: Prisma.$CourtBookingPayload<ExtArgs>[] bookings: Prisma.$CourtBookingPayload<ExtArgs>[]
maintenances: Prisma.$CourtMaintenancePayload<ExtArgs>[]
recurringGroups: Prisma.$RecurringBookingGroupPayload<ExtArgs>[]
} }
scalars: runtime.Types.Extensions.GetPayloadResult<{ scalars: runtime.Types.Extensions.GetPayloadResult<{
id: string id: string
@@ -1154,6 +1530,8 @@ export type $CourtPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs
name: string name: string
slotDurationMinutes: number slotDurationMinutes: number
basePrice: runtime.Decimal basePrice: runtime.Decimal
isUnderMaintenance: boolean
maintenanceReason: string | null
createdAt: Date createdAt: Date
updatedAt: Date updatedAt: Date
}, ExtArgs["result"]["court"]> }, ExtArgs["result"]["court"]>
@@ -1555,6 +1933,8 @@ export interface Prisma__CourtClient<T, Null = never, ExtArgs extends runtime.Ty
availabilities<T extends Prisma.Court$availabilitiesArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Court$availabilitiesArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$CourtAvailabilityPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null> availabilities<T extends Prisma.Court$availabilitiesArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Court$availabilitiesArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$CourtAvailabilityPayload<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> 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>
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.
@@ -1590,6 +1970,8 @@ export interface CourtFieldRefs {
readonly name: Prisma.FieldRef<"Court", 'String'> readonly name: Prisma.FieldRef<"Court", 'String'>
readonly slotDurationMinutes: Prisma.FieldRef<"Court", 'Int'> readonly slotDurationMinutes: Prisma.FieldRef<"Court", 'Int'>
readonly basePrice: Prisma.FieldRef<"Court", 'Decimal'> readonly basePrice: Prisma.FieldRef<"Court", 'Decimal'>
readonly isUnderMaintenance: Prisma.FieldRef<"Court", 'Boolean'>
readonly maintenanceReason: Prisma.FieldRef<"Court", 'String'>
readonly createdAt: Prisma.FieldRef<"Court", 'DateTime'> readonly createdAt: Prisma.FieldRef<"Court", 'DateTime'>
readonly updatedAt: Prisma.FieldRef<"Court", 'DateTime'> readonly updatedAt: Prisma.FieldRef<"Court", 'DateTime'>
} }
@@ -2064,6 +2446,54 @@ export type Court$bookingsArgs<ExtArgs extends runtime.Types.Extensions.Internal
distinct?: Prisma.CourtBookingScalarFieldEnum | Prisma.CourtBookingScalarFieldEnum[] distinct?: Prisma.CourtBookingScalarFieldEnum | Prisma.CourtBookingScalarFieldEnum[]
} }
/**
* Court.maintenances
*/
export type Court$maintenancesArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
/**
* Select specific fields to fetch from the CourtMaintenance
*/
select?: Prisma.CourtMaintenanceSelect<ExtArgs> | null
/**
* Omit specific fields from the CourtMaintenance
*/
omit?: Prisma.CourtMaintenanceOmit<ExtArgs> | null
/**
* Choose, which related nodes to fetch as well
*/
include?: Prisma.CourtMaintenanceInclude<ExtArgs> | null
where?: Prisma.CourtMaintenanceWhereInput
orderBy?: Prisma.CourtMaintenanceOrderByWithRelationInput | Prisma.CourtMaintenanceOrderByWithRelationInput[]
cursor?: Prisma.CourtMaintenanceWhereUniqueInput
take?: number
skip?: number
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
*/ */

View File

@@ -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
*/ */

View File

@@ -31,6 +31,9 @@ export type UserMinAggregateOutputType = {
emailVerified: boolean | null emailVerified: boolean | null
image: string | null image: string | null
phone: string | null phone: string | null
banned: boolean | null
bannedAt: Date | null
banReason: string | null
createdAt: Date | null createdAt: Date | null
updatedAt: Date | null updatedAt: Date | null
role: string | null role: string | null
@@ -43,6 +46,9 @@ export type UserMaxAggregateOutputType = {
emailVerified: boolean | null emailVerified: boolean | null
image: string | null image: string | null
phone: string | null phone: string | null
banned: boolean | null
bannedAt: Date | null
banReason: string | null
createdAt: Date | null createdAt: Date | null
updatedAt: Date | null updatedAt: Date | null
role: string | null role: string | null
@@ -55,6 +61,9 @@ export type UserCountAggregateOutputType = {
emailVerified: number emailVerified: number
image: number image: number
phone: number phone: number
banned: number
bannedAt: number
banReason: number
createdAt: number createdAt: number
updatedAt: number updatedAt: number
role: number role: number
@@ -69,6 +78,9 @@ export type UserMinAggregateInputType = {
emailVerified?: true emailVerified?: true
image?: true image?: true
phone?: true phone?: true
banned?: true
bannedAt?: true
banReason?: true
createdAt?: true createdAt?: true
updatedAt?: true updatedAt?: true
role?: true role?: true
@@ -81,6 +93,9 @@ export type UserMaxAggregateInputType = {
emailVerified?: true emailVerified?: true
image?: true image?: true
phone?: true phone?: true
banned?: true
bannedAt?: true
banReason?: true
createdAt?: true createdAt?: true
updatedAt?: true updatedAt?: true
role?: true role?: true
@@ -93,6 +108,9 @@ export type UserCountAggregateInputType = {
emailVerified?: true emailVerified?: true
image?: true image?: true
phone?: true phone?: true
banned?: true
bannedAt?: true
banReason?: true
createdAt?: true createdAt?: true
updatedAt?: true updatedAt?: true
role?: true role?: true
@@ -178,6 +196,9 @@ export type UserGroupByOutputType = {
emailVerified: boolean emailVerified: boolean
image: string | null image: string | null
phone: string | null phone: string | null
banned: boolean
bannedAt: Date | null
banReason: string | null
createdAt: Date createdAt: Date
updatedAt: Date updatedAt: Date
role: string role: string
@@ -211,6 +232,9 @@ export type UserWhereInput = {
emailVerified?: Prisma.BoolFilter<"User"> | boolean emailVerified?: Prisma.BoolFilter<"User"> | boolean
image?: Prisma.StringNullableFilter<"User"> | string | null image?: Prisma.StringNullableFilter<"User"> | string | null
phone?: Prisma.StringNullableFilter<"User"> | string | null phone?: Prisma.StringNullableFilter<"User"> | string | null
banned?: Prisma.BoolFilter<"User"> | boolean
bannedAt?: Prisma.DateTimeNullableFilter<"User"> | Date | string | null
banReason?: Prisma.StringNullableFilter<"User"> | string | null
createdAt?: Prisma.DateTimeFilter<"User"> | Date | string createdAt?: Prisma.DateTimeFilter<"User"> | Date | string
updatedAt?: Prisma.DateTimeFilter<"User"> | Date | string updatedAt?: Prisma.DateTimeFilter<"User"> | Date | string
role?: Prisma.StringFilter<"User"> | string role?: Prisma.StringFilter<"User"> | string
@@ -226,6 +250,9 @@ export type UserOrderByWithRelationInput = {
emailVerified?: Prisma.SortOrder emailVerified?: Prisma.SortOrder
image?: Prisma.SortOrderInput | Prisma.SortOrder image?: Prisma.SortOrderInput | Prisma.SortOrder
phone?: Prisma.SortOrderInput | Prisma.SortOrder phone?: Prisma.SortOrderInput | Prisma.SortOrder
banned?: Prisma.SortOrder
bannedAt?: Prisma.SortOrderInput | Prisma.SortOrder
banReason?: Prisma.SortOrderInput | Prisma.SortOrder
createdAt?: Prisma.SortOrder createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder updatedAt?: Prisma.SortOrder
role?: Prisma.SortOrder role?: Prisma.SortOrder
@@ -244,6 +271,9 @@ export type UserWhereUniqueInput = Prisma.AtLeast<{
emailVerified?: Prisma.BoolFilter<"User"> | boolean emailVerified?: Prisma.BoolFilter<"User"> | boolean
image?: Prisma.StringNullableFilter<"User"> | string | null image?: Prisma.StringNullableFilter<"User"> | string | null
phone?: Prisma.StringNullableFilter<"User"> | string | null phone?: Prisma.StringNullableFilter<"User"> | string | null
banned?: Prisma.BoolFilter<"User"> | boolean
bannedAt?: Prisma.DateTimeNullableFilter<"User"> | Date | string | null
banReason?: Prisma.StringNullableFilter<"User"> | string | null
createdAt?: Prisma.DateTimeFilter<"User"> | Date | string createdAt?: Prisma.DateTimeFilter<"User"> | Date | string
updatedAt?: Prisma.DateTimeFilter<"User"> | Date | string updatedAt?: Prisma.DateTimeFilter<"User"> | Date | string
role?: Prisma.StringFilter<"User"> | string role?: Prisma.StringFilter<"User"> | string
@@ -259,6 +289,9 @@ export type UserOrderByWithAggregationInput = {
emailVerified?: Prisma.SortOrder emailVerified?: Prisma.SortOrder
image?: Prisma.SortOrderInput | Prisma.SortOrder image?: Prisma.SortOrderInput | Prisma.SortOrder
phone?: Prisma.SortOrderInput | Prisma.SortOrder phone?: Prisma.SortOrderInput | Prisma.SortOrder
banned?: Prisma.SortOrder
bannedAt?: Prisma.SortOrderInput | Prisma.SortOrder
banReason?: Prisma.SortOrderInput | Prisma.SortOrder
createdAt?: Prisma.SortOrder createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder updatedAt?: Prisma.SortOrder
role?: Prisma.SortOrder role?: Prisma.SortOrder
@@ -277,6 +310,9 @@ export type UserScalarWhereWithAggregatesInput = {
emailVerified?: Prisma.BoolWithAggregatesFilter<"User"> | boolean emailVerified?: Prisma.BoolWithAggregatesFilter<"User"> | boolean
image?: Prisma.StringNullableWithAggregatesFilter<"User"> | string | null image?: Prisma.StringNullableWithAggregatesFilter<"User"> | string | null
phone?: Prisma.StringNullableWithAggregatesFilter<"User"> | string | null phone?: Prisma.StringNullableWithAggregatesFilter<"User"> | string | null
banned?: Prisma.BoolWithAggregatesFilter<"User"> | boolean
bannedAt?: Prisma.DateTimeNullableWithAggregatesFilter<"User"> | Date | string | null
banReason?: Prisma.StringNullableWithAggregatesFilter<"User"> | string | null
createdAt?: Prisma.DateTimeWithAggregatesFilter<"User"> | Date | string createdAt?: Prisma.DateTimeWithAggregatesFilter<"User"> | Date | string
updatedAt?: Prisma.DateTimeWithAggregatesFilter<"User"> | Date | string updatedAt?: Prisma.DateTimeWithAggregatesFilter<"User"> | Date | string
role?: Prisma.StringWithAggregatesFilter<"User"> | string role?: Prisma.StringWithAggregatesFilter<"User"> | string
@@ -289,6 +325,9 @@ export type UserCreateInput = {
emailVerified?: boolean emailVerified?: boolean
image?: string | null image?: string | null
phone?: string | null phone?: string | null
banned?: boolean
bannedAt?: Date | string | null
banReason?: string | null
createdAt?: Date | string createdAt?: Date | string
updatedAt?: Date | string updatedAt?: Date | string
role?: string role?: string
@@ -304,6 +343,9 @@ export type UserUncheckedCreateInput = {
emailVerified?: boolean emailVerified?: boolean
image?: string | null image?: string | null
phone?: string | null phone?: string | null
banned?: boolean
bannedAt?: Date | string | null
banReason?: string | null
createdAt?: Date | string createdAt?: Date | string
updatedAt?: Date | string updatedAt?: Date | string
role?: string role?: string
@@ -319,6 +361,9 @@ export type UserUpdateInput = {
emailVerified?: Prisma.BoolFieldUpdateOperationsInput | boolean emailVerified?: Prisma.BoolFieldUpdateOperationsInput | boolean
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
phone?: Prisma.NullableStringFieldUpdateOperationsInput | string | null phone?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
banned?: Prisma.BoolFieldUpdateOperationsInput | boolean
bannedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
banReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
role?: Prisma.StringFieldUpdateOperationsInput | string role?: Prisma.StringFieldUpdateOperationsInput | string
@@ -334,6 +379,9 @@ export type UserUncheckedUpdateInput = {
emailVerified?: Prisma.BoolFieldUpdateOperationsInput | boolean emailVerified?: Prisma.BoolFieldUpdateOperationsInput | boolean
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
phone?: Prisma.NullableStringFieldUpdateOperationsInput | string | null phone?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
banned?: Prisma.BoolFieldUpdateOperationsInput | boolean
bannedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
banReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
role?: Prisma.StringFieldUpdateOperationsInput | string role?: Prisma.StringFieldUpdateOperationsInput | string
@@ -349,6 +397,9 @@ export type UserCreateManyInput = {
emailVerified?: boolean emailVerified?: boolean
image?: string | null image?: string | null
phone?: string | null phone?: string | null
banned?: boolean
bannedAt?: Date | string | null
banReason?: string | null
createdAt?: Date | string createdAt?: Date | string
updatedAt?: Date | string updatedAt?: Date | string
role?: string role?: string
@@ -361,6 +412,9 @@ export type UserUpdateManyMutationInput = {
emailVerified?: Prisma.BoolFieldUpdateOperationsInput | boolean emailVerified?: Prisma.BoolFieldUpdateOperationsInput | boolean
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
phone?: Prisma.NullableStringFieldUpdateOperationsInput | string | null phone?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
banned?: Prisma.BoolFieldUpdateOperationsInput | boolean
bannedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
banReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
role?: Prisma.StringFieldUpdateOperationsInput | string role?: Prisma.StringFieldUpdateOperationsInput | string
@@ -373,6 +427,9 @@ export type UserUncheckedUpdateManyInput = {
emailVerified?: Prisma.BoolFieldUpdateOperationsInput | boolean emailVerified?: Prisma.BoolFieldUpdateOperationsInput | boolean
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
phone?: Prisma.NullableStringFieldUpdateOperationsInput | string | null phone?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
banned?: Prisma.BoolFieldUpdateOperationsInput | boolean
bannedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
banReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
role?: Prisma.StringFieldUpdateOperationsInput | string role?: Prisma.StringFieldUpdateOperationsInput | string
@@ -385,6 +442,9 @@ export type UserCountOrderByAggregateInput = {
emailVerified?: Prisma.SortOrder emailVerified?: Prisma.SortOrder
image?: Prisma.SortOrder image?: Prisma.SortOrder
phone?: Prisma.SortOrder phone?: Prisma.SortOrder
banned?: Prisma.SortOrder
bannedAt?: Prisma.SortOrder
banReason?: Prisma.SortOrder
createdAt?: Prisma.SortOrder createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder updatedAt?: Prisma.SortOrder
role?: Prisma.SortOrder role?: Prisma.SortOrder
@@ -397,6 +457,9 @@ export type UserMaxOrderByAggregateInput = {
emailVerified?: Prisma.SortOrder emailVerified?: Prisma.SortOrder
image?: Prisma.SortOrder image?: Prisma.SortOrder
phone?: Prisma.SortOrder phone?: Prisma.SortOrder
banned?: Prisma.SortOrder
bannedAt?: Prisma.SortOrder
banReason?: Prisma.SortOrder
createdAt?: Prisma.SortOrder createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder updatedAt?: Prisma.SortOrder
role?: Prisma.SortOrder role?: Prisma.SortOrder
@@ -409,6 +472,9 @@ export type UserMinOrderByAggregateInput = {
emailVerified?: Prisma.SortOrder emailVerified?: Prisma.SortOrder
image?: Prisma.SortOrder image?: Prisma.SortOrder
phone?: Prisma.SortOrder phone?: Prisma.SortOrder
banned?: Prisma.SortOrder
bannedAt?: Prisma.SortOrder
banReason?: Prisma.SortOrder
createdAt?: Prisma.SortOrder createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder updatedAt?: Prisma.SortOrder
role?: Prisma.SortOrder role?: Prisma.SortOrder
@@ -431,6 +497,10 @@ export type NullableStringFieldUpdateOperationsInput = {
set?: string | null set?: string | null
} }
export type NullableDateTimeFieldUpdateOperationsInput = {
set?: Date | string | null
}
export type DateTimeFieldUpdateOperationsInput = { export type DateTimeFieldUpdateOperationsInput = {
set?: Date | string set?: Date | string
} }
@@ -484,6 +554,9 @@ export type UserCreateWithoutSessionsInput = {
emailVerified?: boolean emailVerified?: boolean
image?: string | null image?: string | null
phone?: string | null phone?: string | null
banned?: boolean
bannedAt?: Date | string | null
banReason?: string | null
createdAt?: Date | string createdAt?: Date | string
updatedAt?: Date | string updatedAt?: Date | string
role?: string role?: string
@@ -498,6 +571,9 @@ export type UserUncheckedCreateWithoutSessionsInput = {
emailVerified?: boolean emailVerified?: boolean
image?: string | null image?: string | null
phone?: string | null phone?: string | null
banned?: boolean
bannedAt?: Date | string | null
banReason?: string | null
createdAt?: Date | string createdAt?: Date | string
updatedAt?: Date | string updatedAt?: Date | string
role?: string role?: string
@@ -528,6 +604,9 @@ export type UserUpdateWithoutSessionsInput = {
emailVerified?: Prisma.BoolFieldUpdateOperationsInput | boolean emailVerified?: Prisma.BoolFieldUpdateOperationsInput | boolean
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
phone?: Prisma.NullableStringFieldUpdateOperationsInput | string | null phone?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
banned?: Prisma.BoolFieldUpdateOperationsInput | boolean
bannedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
banReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
role?: Prisma.StringFieldUpdateOperationsInput | string role?: Prisma.StringFieldUpdateOperationsInput | string
@@ -542,6 +621,9 @@ export type UserUncheckedUpdateWithoutSessionsInput = {
emailVerified?: Prisma.BoolFieldUpdateOperationsInput | boolean emailVerified?: Prisma.BoolFieldUpdateOperationsInput | boolean
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
phone?: Prisma.NullableStringFieldUpdateOperationsInput | string | null phone?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
banned?: Prisma.BoolFieldUpdateOperationsInput | boolean
bannedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
banReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
role?: Prisma.StringFieldUpdateOperationsInput | string role?: Prisma.StringFieldUpdateOperationsInput | string
@@ -556,6 +638,9 @@ export type UserCreateWithoutAccountsInput = {
emailVerified?: boolean emailVerified?: boolean
image?: string | null image?: string | null
phone?: string | null phone?: string | null
banned?: boolean
bannedAt?: Date | string | null
banReason?: string | null
createdAt?: Date | string createdAt?: Date | string
updatedAt?: Date | string updatedAt?: Date | string
role?: string role?: string
@@ -570,6 +655,9 @@ export type UserUncheckedCreateWithoutAccountsInput = {
emailVerified?: boolean emailVerified?: boolean
image?: string | null image?: string | null
phone?: string | null phone?: string | null
banned?: boolean
bannedAt?: Date | string | null
banReason?: string | null
createdAt?: Date | string createdAt?: Date | string
updatedAt?: Date | string updatedAt?: Date | string
role?: string role?: string
@@ -600,6 +688,9 @@ export type UserUpdateWithoutAccountsInput = {
emailVerified?: Prisma.BoolFieldUpdateOperationsInput | boolean emailVerified?: Prisma.BoolFieldUpdateOperationsInput | boolean
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
phone?: Prisma.NullableStringFieldUpdateOperationsInput | string | null phone?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
banned?: Prisma.BoolFieldUpdateOperationsInput | boolean
bannedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
banReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
role?: Prisma.StringFieldUpdateOperationsInput | string role?: Prisma.StringFieldUpdateOperationsInput | string
@@ -614,6 +705,9 @@ export type UserUncheckedUpdateWithoutAccountsInput = {
emailVerified?: Prisma.BoolFieldUpdateOperationsInput | boolean emailVerified?: Prisma.BoolFieldUpdateOperationsInput | boolean
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
phone?: Prisma.NullableStringFieldUpdateOperationsInput | string | null phone?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
banned?: Prisma.BoolFieldUpdateOperationsInput | boolean
bannedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
banReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
role?: Prisma.StringFieldUpdateOperationsInput | string role?: Prisma.StringFieldUpdateOperationsInput | string
@@ -628,6 +722,9 @@ export type UserCreateWithoutComplexesInput = {
emailVerified?: boolean emailVerified?: boolean
image?: string | null image?: string | null
phone?: string | null phone?: string | null
banned?: boolean
bannedAt?: Date | string | null
banReason?: string | null
createdAt?: Date | string createdAt?: Date | string
updatedAt?: Date | string updatedAt?: Date | string
role?: string role?: string
@@ -642,6 +739,9 @@ export type UserUncheckedCreateWithoutComplexesInput = {
emailVerified?: boolean emailVerified?: boolean
image?: string | null image?: string | null
phone?: string | null phone?: string | null
banned?: boolean
bannedAt?: Date | string | null
banReason?: string | null
createdAt?: Date | string createdAt?: Date | string
updatedAt?: Date | string updatedAt?: Date | string
role?: string role?: string
@@ -672,6 +772,9 @@ export type UserUpdateWithoutComplexesInput = {
emailVerified?: Prisma.BoolFieldUpdateOperationsInput | boolean emailVerified?: Prisma.BoolFieldUpdateOperationsInput | boolean
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
phone?: Prisma.NullableStringFieldUpdateOperationsInput | string | null phone?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
banned?: Prisma.BoolFieldUpdateOperationsInput | boolean
bannedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
banReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
role?: Prisma.StringFieldUpdateOperationsInput | string role?: Prisma.StringFieldUpdateOperationsInput | string
@@ -686,6 +789,9 @@ export type UserUncheckedUpdateWithoutComplexesInput = {
emailVerified?: Prisma.BoolFieldUpdateOperationsInput | boolean emailVerified?: Prisma.BoolFieldUpdateOperationsInput | boolean
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
phone?: Prisma.NullableStringFieldUpdateOperationsInput | string | null phone?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
banned?: Prisma.BoolFieldUpdateOperationsInput | boolean
bannedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
banReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
role?: Prisma.StringFieldUpdateOperationsInput | string role?: Prisma.StringFieldUpdateOperationsInput | string
@@ -749,6 +855,9 @@ export type UserSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = r
emailVerified?: boolean emailVerified?: boolean
image?: boolean image?: boolean
phone?: boolean phone?: boolean
banned?: boolean
bannedAt?: boolean
banReason?: boolean
createdAt?: boolean createdAt?: boolean
updatedAt?: boolean updatedAt?: boolean
role?: boolean role?: boolean
@@ -765,6 +874,9 @@ export type UserSelectCreateManyAndReturn<ExtArgs extends runtime.Types.Extensio
emailVerified?: boolean emailVerified?: boolean
image?: boolean image?: boolean
phone?: boolean phone?: boolean
banned?: boolean
bannedAt?: boolean
banReason?: boolean
createdAt?: boolean createdAt?: boolean
updatedAt?: boolean updatedAt?: boolean
role?: boolean role?: boolean
@@ -777,6 +889,9 @@ export type UserSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensio
emailVerified?: boolean emailVerified?: boolean
image?: boolean image?: boolean
phone?: boolean phone?: boolean
banned?: boolean
bannedAt?: boolean
banReason?: boolean
createdAt?: boolean createdAt?: boolean
updatedAt?: boolean updatedAt?: boolean
role?: boolean role?: boolean
@@ -789,12 +904,15 @@ export type UserSelectScalar = {
emailVerified?: boolean emailVerified?: boolean
image?: boolean image?: boolean
phone?: boolean phone?: boolean
banned?: boolean
bannedAt?: boolean
banReason?: boolean
createdAt?: boolean createdAt?: boolean
updatedAt?: boolean updatedAt?: boolean
role?: boolean role?: boolean
} }
export type UserOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "name" | "email" | "emailVerified" | "image" | "phone" | "createdAt" | "updatedAt" | "role", ExtArgs["result"]["user"]> export type UserOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "name" | "email" | "emailVerified" | "image" | "phone" | "banned" | "bannedAt" | "banReason" | "createdAt" | "updatedAt" | "role", ExtArgs["result"]["user"]>
export type UserInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = { export type UserInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
sessions?: boolean | Prisma.User$sessionsArgs<ExtArgs> sessions?: boolean | Prisma.User$sessionsArgs<ExtArgs>
accounts?: boolean | Prisma.User$accountsArgs<ExtArgs> accounts?: boolean | Prisma.User$accountsArgs<ExtArgs>
@@ -818,6 +936,9 @@ export type $UserPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs =
emailVerified: boolean emailVerified: boolean
image: string | null image: string | null
phone: string | null phone: string | null
banned: boolean
bannedAt: Date | null
banReason: string | null
createdAt: Date createdAt: Date
updatedAt: Date updatedAt: Date
role: string role: string
@@ -1253,6 +1374,9 @@ export interface UserFieldRefs {
readonly emailVerified: Prisma.FieldRef<"User", 'Boolean'> readonly emailVerified: Prisma.FieldRef<"User", 'Boolean'>
readonly image: Prisma.FieldRef<"User", 'String'> readonly image: Prisma.FieldRef<"User", 'String'>
readonly phone: Prisma.FieldRef<"User", 'String'> readonly phone: Prisma.FieldRef<"User", 'String'>
readonly banned: Prisma.FieldRef<"User", 'Boolean'>
readonly bannedAt: Prisma.FieldRef<"User", 'DateTime'>
readonly banReason: Prisma.FieldRef<"User", 'String'>
readonly createdAt: Prisma.FieldRef<"User", 'DateTime'> readonly createdAt: Prisma.FieldRef<"User", 'DateTime'>
readonly updatedAt: Prisma.FieldRef<"User", 'DateTime'> readonly updatedAt: Prisma.FieldRef<"User", 'DateTime'>
readonly role: Prisma.FieldRef<"User", 'String'> readonly role: Prisma.FieldRef<"User", 'String'>

View File

@@ -22,10 +22,26 @@ export const auth = betterAuth({
}, },
user: { user: {
additionalFields: { additionalFields: {
role: {
type: 'string',
required: false,
},
phone: { phone: {
type: 'string', type: 'string',
required: false, required: false,
}, },
banned: {
type: 'boolean',
required: false,
},
bannedAt: {
type: 'string',
required: false,
},
banReason: {
type: 'string',
required: false,
},
}, },
}, },
emailVerification: { emailVerification: {

View File

@@ -0,0 +1,68 @@
type IpGeoInfo = {
ip: string;
city: string;
country: string;
countryCode: string;
lat: number | null;
lon: number | null;
};
const cache = new Map<string, { data: IpGeoInfo; expiresAt: number }>();
const CACHE_TTL_MS = 60 * 60 * 1000;
function getCached(ip: string): IpGeoInfo | undefined {
const entry = cache.get(ip);
if (!entry) return undefined;
if (Date.now() > entry.expiresAt) {
cache.delete(ip);
return undefined;
}
return entry.data;
}
function setCache(ip: string, info: IpGeoInfo): void {
cache.set(ip, { data: info, expiresAt: Date.now() + CACHE_TTL_MS });
}
async function fetchGeoInfo(ip: string): Promise<IpGeoInfo | null> {
if (ip === '127.0.0.1' || ip === '::1' || ip.startsWith('192.168.') || ip.startsWith('10.')) {
return {
ip,
city: 'Red local',
country: 'Red local',
countryCode: 'LOCAL',
lat: null,
lon: null,
};
}
const cached = getCached(ip);
if (cached) return cached;
try {
const response = await fetch(
`http://ip-api.com/json/${ip}?fields=status,country,countryCode,city,lat,lon,query`
);
if (!response.ok) return null;
const data = await response.json();
if (data.status !== 'success') return null;
const info: IpGeoInfo = {
ip: data.query,
city: data.city || 'Desconocida',
country: data.country || 'Desconocido',
countryCode: data.countryCode || '',
lat: data.lat ?? null,
lon: data.lon ?? null,
};
setCache(ip, info);
return info;
} catch {
return null;
}
}
export type { IpGeoInfo };
export { fetchGeoInfo };

View File

@@ -1,7 +1,20 @@
import { auth } from '@/lib/auth'; import { auth } from '@/lib/auth';
import { fetchGeoInfo } from '@/lib/geoip';
import { db } from '@/lib/prisma';
import type { AppEnv } from '@/types/hono'; import type { AppEnv } from '@/types/hono';
import { createMiddleware } from 'hono/factory'; import { createMiddleware } from 'hono/factory';
type SessionWithGeo = {
id: string;
ipAddress: string | null;
userAgent: string | null;
country: string | null;
city: string | null;
countryCode: string | null;
latitude: number | null;
longitude: number | null;
};
export const requireAuth = createMiddleware<AppEnv>(async (c, next) => { export const requireAuth = createMiddleware<AppEnv>(async (c, next) => {
const session = await auth.api.getSession({ const session = await auth.api.getSession({
headers: c.req.raw.headers, headers: c.req.raw.headers,
@@ -11,8 +24,45 @@ export const requireAuth = createMiddleware<AppEnv>(async (c, next) => {
return c.json({ message: 'Unauthorized' }, 401); return c.json({ message: 'Unauthorized' }, 401);
} }
const user = session.user as { banned?: boolean; banReason?: string | null };
if (user.banned) {
return c.json(
{
message: 'Tu cuenta ha sido bloqueada.',
...(user.banReason ? { reason: user.banReason } : {}),
},
403
);
}
const s = session.session as unknown as SessionWithGeo;
c.set('user', session.user); c.set('user', session.user);
c.set('session', session.session); c.set('session', session.session);
if (s.ipAddress && !s.country) {
resolveSessionGeo(s.id, s.ipAddress);
}
await next(); await next();
}); });
async function resolveSessionGeo(sessionId: string, ipAddress: string) {
try {
const geo = await fetchGeoInfo(ipAddress);
if (!geo) return;
await db.session.update({
where: { id: sessionId },
data: {
country: geo.country,
city: geo.city,
countryCode: geo.countryCode,
latitude: geo.lat,
longitude: geo.lon,
},
});
} catch {
// Silently fail — geo enrichment is best-effort
}
}

View File

@@ -1,13 +1,19 @@
import { requireAuth } from '@/middlewares/require-auth.middleware'; import { requireAuth } from '@/middlewares/require-auth.middleware';
import { cancelRecurringGroupHandler } from '@/modules/admin-booking/handlers/cancel-recurring-group.handler';
import { createAdminBookingHandler } from '@/modules/admin-booking/handlers/create-admin-booking.handler'; import { createAdminBookingHandler } from '@/modules/admin-booking/handlers/create-admin-booking.handler';
import { createAdminRecurringBookingHandler } from '@/modules/admin-booking/handlers/create-admin-recurring-booking.handler';
import { listAdminBookingsHandler } from '@/modules/admin-booking/handlers/list-admin-bookings.handler'; import { listAdminBookingsHandler } from '@/modules/admin-booking/handlers/list-admin-bookings.handler';
import { listRecurringGroupsHandler } from '@/modules/admin-booking/handlers/list-recurring-groups.handler';
import { updateAdminBookingStatusHandler } from '@/modules/admin-booking/handlers/update-admin-booking-status.handler'; import { updateAdminBookingStatusHandler } from '@/modules/admin-booking/handlers/update-admin-booking-status.handler';
import { updateRecurringGroupHandler } from '@/modules/admin-booking/handlers/update-recurring-group.handler';
import type { AppEnv } from '@/types/hono'; import type { AppEnv } from '@/types/hono';
import { zValidator } from '@hono/zod-validator'; import { zValidator } from '@hono/zod-validator';
import { import {
createAdminBookingSchema, createAdminBookingSchema,
createRecurringBookingSchema,
listAdminBookingsQuerySchema, listAdminBookingsQuerySchema,
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 +21,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,6 +39,32 @@ 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),

View File

@@ -0,0 +1,20 @@
import { AdminBookingServiceError } from '@/modules/admin-booking/services/admin-booking.service';
import { cancelRecurringGroup } from '@/modules/admin-booking/services/admin-recurring-booking.service';
import type { AppContext } from '@/types/hono';
type GroupIdParams = { groupId: string };
export async function cancelRecurringGroupHandler(c: AppContext) {
const { groupId } = c.req.valid('param' as never) as GroupIdParams;
const user = c.get('user');
try {
const result = await cancelRecurringGroup(user.id, groupId);
return c.json(result);
} catch (error) {
if (error instanceof AdminBookingServiceError) {
return c.json({ message: error.message }, error.status);
}
throw error;
}
}

View File

@@ -0,0 +1,40 @@
import { AdminBookingServiceError } from '@/modules/admin-booking/services/admin-booking.service';
import { createAdminRecurringBooking } from '@/modules/admin-booking/services/admin-recurring-booking.service';
import { sendBookingConfirmation } from '@/services/booking-email.service';
import type { AppContext } from '@/types/hono';
import type { CreateRecurringBookingInput } from '@repo/api-contract';
type ComplexIdParams = { complexId: string };
export async function createAdminRecurringBookingHandler(c: AppContext) {
const { complexId } = c.req.valid('param' as never) as ComplexIdParams;
const payload = c.req.valid('json' as never) as CreateRecurringBookingInput;
const user = c.get('user');
try {
const group = await createAdminRecurringBooking(user.id, complexId, payload);
const firstBooking = group.bookings[0];
if (firstBooking?.customerEmail) {
void sendBookingConfirmation({
bookingCode: firstBooking.bookingCode,
complexName: firstBooking.complexName,
date: firstBooking.date,
startTime: firstBooking.startTime,
endTime: firstBooking.endTime,
courtName: firstBooking.courtName,
sportName: firstBooking.sport.name,
customerName: firstBooking.customerName,
customerEmail: firstBooking.customerEmail,
price: firstBooking.price,
});
}
return c.json(group, 201);
} catch (error) {
if (error instanceof AdminBookingServiceError) {
return c.json({ message: error.message }, error.status);
}
throw error;
}
}

View File

@@ -0,0 +1,19 @@
import { AdminBookingServiceError } from '@/modules/admin-booking/services/admin-booking.service';
import { listRecurringGroups } from '@/modules/admin-booking/services/admin-recurring-booking.service';
import type { AppContext } from '@/types/hono';
type ComplexIdParams = { complexId: string };
export async function listRecurringGroupsHandler(c: AppContext) {
const { complexId } = c.req.valid('param' as never) as ComplexIdParams;
try {
const groups = await listRecurringGroups(complexId);
return c.json({ groups });
} catch (error) {
if (error instanceof AdminBookingServiceError) {
return c.json({ message: error.message }, error.status);
}
throw error;
}
}

View File

@@ -0,0 +1,22 @@
import { AdminBookingServiceError } from '@/modules/admin-booking/services/admin-booking.service';
import { updateRecurringGroup } from '@/modules/admin-booking/services/admin-recurring-booking.service';
import type { AppContext } from '@/types/hono';
import type { UpdateRecurringGroupInput } from '@repo/api-contract';
type GroupIdParams = { groupId: string };
export async function updateRecurringGroupHandler(c: AppContext) {
const { groupId } = c.req.valid('param' as never) as GroupIdParams;
const payload = c.req.valid('json' as never) as UpdateRecurringGroupInput;
const user = c.get('user');
try {
const group = await updateRecurringGroup(user.id, groupId, payload);
return c.json(group);
} catch (error) {
if (error instanceof AdminBookingServiceError) {
return c.json({ message: error.message }, error.status);
}
throw error;
}
}

View File

@@ -202,6 +202,7 @@ function mapBookingResponse(booking: {
customerPhone: string; customerPhone: string;
customerEmail: string; customerEmail: string;
status: 'CONFIRMED' | 'CANCELLED' | 'COMPLETED' | 'NOSHOW'; status: 'CONFIRMED' | 'CANCELLED' | 'COMPLETED' | 'NOSHOW';
recurringGroupId: string | null;
createdAt: Date; createdAt: Date;
updatedAt: Date; updatedAt: Date;
court: { court: {
@@ -239,6 +240,7 @@ function mapBookingResponse(booking: {
customerEmail: booking.customerEmail, customerEmail: booking.customerEmail,
price: booking.price ?? 0, price: booking.price ?? 0,
status: booking.status, status: booking.status,
recurringGroupId: booking.recurringGroupId,
createdAt: booking.createdAt.toISOString(), createdAt: booking.createdAt.toISOString(),
updatedAt: booking.updatedAt.toISOString(), updatedAt: booking.updatedAt.toISOString(),
}; };
@@ -431,6 +433,7 @@ export async function createAdminBooking(
customerPhone: input.customerPhone.trim(), customerPhone: input.customerPhone.trim(),
customerEmail: input.customerEmail?.trim() ?? '', customerEmail: input.customerEmail?.trim() ?? '',
status: 'CONFIRMED', status: 'CONFIRMED',
recurringGroupId: null,
}, },
include: { include: {
court: { court: {

View File

@@ -0,0 +1,893 @@
import { randomInt } from 'node:crypto';
import { DayOfWeek as DayOfWeekEnum } from '@/generated/prisma/enums';
import { db } from '@/lib/prisma';
import { isSlotInPast } from '@/lib/slot-validator';
import {
evaluatePlanUsage,
isFeatureEnabled,
parsePlanRules,
} from '@/modules/plan/services/plan-rules.service';
import type {
CreateRecurringBookingInput,
RecurringBookingGroup,
UpdateRecurringGroupInput,
} from '@repo/api-contract';
import { v7 as uuidv7 } from 'uuid';
import { AdminBookingServiceError } from './admin-booking.service';
const BOOKING_CODE_ALPHABET = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
const BOOKING_CODE_LENGTH = 6;
const MAX_RECURRING_WEEKS = 52;
const DAY_INDEX_BY_VALUE: Record<string, number> = {
SUNDAY: 0,
MONDAY: 1,
TUESDAY: 2,
WEDNESDAY: 3,
THURSDAY: 4,
FRIDAY: 5,
SATURDAY: 6,
};
const DAY_OF_WEEK_BY_INDEX = [
DayOfWeekEnum.SUNDAY,
DayOfWeekEnum.MONDAY,
DayOfWeekEnum.TUESDAY,
DayOfWeekEnum.WEDNESDAY,
DayOfWeekEnum.THURSDAY,
DayOfWeekEnum.FRIDAY,
DayOfWeekEnum.SATURDAY,
] as const;
type DayOfWeek = (typeof DAY_OF_WEEK_BY_INDEX)[number];
function toMinutes(value: string): number {
const [hours, minutes] = value.split(':').map((part) => Number(part));
return hours * 60 + minutes;
}
function minutesToTime(minutes: number): string {
const safeMinutes = Math.max(0, minutes);
const hours = Math.floor(safeMinutes / 60);
const mins = safeMinutes % 60;
return `${String(hours).padStart(2, '0')}:${String(mins).padStart(2, '0')}`;
}
function parseIsoDate(date: string): Date {
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(date);
if (!match) {
throw new AdminBookingServiceError('La fecha debe tener formato YYYY-MM-DD.', 400);
}
const year = Number(match[1]);
const month = Number(match[2]);
const day = Number(match[3]);
const parsed = new Date(Date.UTC(year, month - 1, day));
if (
Number.isNaN(parsed.getTime()) ||
parsed.getUTCFullYear() !== year ||
parsed.getUTCMonth() + 1 !== month ||
parsed.getUTCDate() !== day
) {
throw new AdminBookingServiceError('La fecha enviada no es valida.', 400);
}
return parsed;
}
function formatIsoDate(date: Date): string {
return date.toISOString().slice(0, 10);
}
function getDayOfWeekValue(date: Date): DayOfWeek {
const dayOfWeek = DAY_OF_WEEK_BY_INDEX[date.getUTCDay()];
if (!dayOfWeek) {
throw new AdminBookingServiceError('No se pudo resolver el dia de la semana.', 400);
}
return dayOfWeek;
}
function generateBookingCode(): string {
let code = '';
for (let index = 0; index < BOOKING_CODE_LENGTH; index += 1) {
code += BOOKING_CODE_ALPHABET[randomInt(0, BOOKING_CODE_ALPHABET.length)];
}
return code;
}
function buildSlots(
availability: Array<{
startTime: string;
endTime: string;
}>,
slotDurationMinutes: number
) {
const slots: Array<{ startTime: string; endTime: string }> = [];
for (const range of availability) {
const start = toMinutes(range.startTime);
const end = toMinutes(range.endTime);
for (
let current = start;
current + slotDurationMinutes <= end;
current += slotDurationMinutes
) {
slots.push({
startTime: minutesToTime(current),
endTime: minutesToTime(current + slotDurationMinutes),
});
}
}
return slots;
}
async function ensureComplexAccess(complexId: string, userId: string) {
const complexUser = await db.complexUser.findUnique({
where: {
complexId_userId: {
complexId,
userId,
},
},
include: {
complex: {
select: {
id: true,
complexName: true,
plan: {
select: {
rules: true,
},
},
},
},
},
});
if (!complexUser) {
throw new AdminBookingServiceError('No tienes permisos para administrar este complejo.', 403);
}
return complexUser.complex;
}
function* generateRecurringDates(
startDate: Date,
endDate: Date | null,
dayOfWeek: number
): Generator<Date> {
const current = new Date(startDate);
current.setUTCDate(current.getUTCDate() + ((dayOfWeek - current.getUTCDay() + 7) % 7));
if (current < startDate) {
current.setUTCDate(current.getUTCDate() + 7);
}
const maxDate = endDate ?? new Date(startDate);
if (!endDate) {
maxDate.setUTCDate(maxDate.getUTCDate() + MAX_RECURRING_WEEKS * 7);
}
while (current <= maxDate) {
yield new Date(current);
current.setUTCDate(current.getUTCDate() + 7);
}
}
function mapBookingResponse(booking: {
id: string;
bookingCode: string;
bookingDate: Date;
startTime: string;
endTime: string;
customerName: string;
customerPhone: string;
customerEmail: string;
status: 'CONFIRMED' | 'CANCELLED' | 'COMPLETED' | 'NOSHOW';
recurringGroupId: string | null;
createdAt: Date;
updatedAt: Date;
court: {
id: string;
name: string;
complex: {
id: string;
complexName: string;
};
sport: {
id: string;
name: string;
slug: string;
};
};
price?: number;
}) {
return {
id: booking.id,
bookingCode: booking.bookingCode,
complexId: booking.court.complex.id,
complexName: booking.court.complex.complexName,
courtId: booking.court.id,
courtName: booking.court.name,
sport: {
id: booking.court.sport.id,
name: booking.court.sport.name,
slug: booking.court.sport.slug,
},
date: formatIsoDate(booking.bookingDate),
startTime: booking.startTime,
endTime: booking.endTime,
customerName: booking.customerName,
customerPhone: booking.customerPhone,
customerEmail: booking.customerEmail,
price: booking.price ?? 0,
status: booking.status,
recurringGroupId: booking.recurringGroupId,
createdAt: booking.createdAt.toISOString(),
updatedAt: booking.updatedAt.toISOString(),
};
}
export async function createAdminRecurringBooking(
userId: string,
complexId: string,
input: CreateRecurringBookingInput
): Promise<RecurringBookingGroup> {
const complex = await ensureComplexAccess(complexId, userId);
const adminUser = await db.user.findUnique({
where: { id: userId },
select: { emailVerified: true },
});
if (!adminUser?.emailVerified) {
throw new AdminBookingServiceError('Debés verificar tu email para poder crear reservas.', 403);
}
if (!complex.plan) {
throw new AdminBookingServiceError('El complejo no tiene un plan asignado.', 403);
}
const rules = parsePlanRules(complex.plan.rules);
if (!isFeatureEnabled(rules, 'fixedSlots')) {
throw new AdminBookingServiceError(
'Tu plan no permite la creación de turnos fijos. Comunicate con el administrador.',
403
);
}
const startDate = parseIsoDate(input.date);
const dayOfWeek = getDayOfWeekValue(startDate);
const endDate = input.recurringEndDate ? parseIsoDate(input.recurringEndDate) : null;
if (endDate && endDate <= startDate) {
throw new AdminBookingServiceError(
'La fecha de fin debe ser posterior a la fecha de inicio.',
400
);
}
const court = await db.court.findFirst({
where: {
id: input.courtId,
complexId,
},
include: {
availabilities: {
where: {
dayOfWeek,
},
orderBy: {
startTime: 'asc',
},
},
sport: {
select: {
id: true,
name: true,
slug: true,
},
},
priceRules: {
where: { isActive: true },
orderBy: [{ dayOfWeek: 'asc' }, { startTime: 'asc' }],
},
},
});
if (!court) {
throw new AdminBookingServiceError('La cancha seleccionada no existe en el complejo.', 404);
}
const validSlots = buildSlots(court.availabilities, court.slotDurationMinutes);
const selectedEndMinutes = toMinutes(input.startTime) + court.slotDurationMinutes;
const selectedSlot = {
startTime: input.startTime,
endTime: minutesToTime(selectedEndMinutes),
};
const slotExists = validSlots.some(
(slot) => slot.startTime === selectedSlot.startTime && slot.endTime === selectedSlot.endTime
);
if (!slotExists) {
throw new AdminBookingServiceError(
'El horario seleccionado no esta disponible para esa cancha.',
409
);
}
if (isSlotInPast(startDate, input.startTime, court.slotDurationMinutes)) {
throw new AdminBookingServiceError('La fecha de inicio no puede estar en el pasado.', 400);
}
const recurringDates = Array.from(
generateRecurringDates(startDate, endDate, startDate.getUTCDay())
);
if (recurringDates.length === 0) {
throw new AdminBookingServiceError(
'No se generaron fechas para la reserva periódica. Verifica las fechas ingresadas.',
400
);
}
for (let attempt = 0; attempt < 5; attempt += 1) {
try {
const result = await db.$transaction(async (tx) => {
const groupId = uuidv7();
const bookingsForDate = await tx.courtBooking.count({
where: {
startTime: selectedSlot.startTime,
bookingDate: startDate,
status: 'CONFIRMED',
court: {
complexId,
},
},
});
const courtsCount = await tx.court.count({
where: { complexId },
});
const violations = evaluatePlanUsage(rules, {
courtsCount,
bookingsToday: bookingsForDate,
});
const maxBookingsViolation = violations.find(
(v) => v.code === 'MAX_BOOKINGS_PER_DAY_REACHED'
);
if (maxBookingsViolation) {
throw new AdminBookingServiceError(maxBookingsViolation.message, 409);
}
const group = await tx.recurringBookingGroup.create({
data: {
id: groupId,
complexId,
courtId: court.id,
startTime: selectedSlot.startTime,
endTime: selectedSlot.endTime,
dayOfWeek,
startDate,
endDate,
status: 'ACTIVE',
customerName: input.customerName.trim(),
customerPhone: input.customerPhone.trim(),
customerEmail: input.customerEmail?.trim() ?? '',
},
});
const createdBookings = [];
for (const date of recurringDates) {
if (isSlotInPast(date, input.startTime, court.slotDurationMinutes)) {
continue;
}
const overlappingBooking = await tx.courtBooking.findFirst({
where: {
courtId: court.id,
bookingDate: date,
status: 'CONFIRMED',
startTime: {
lt: selectedSlot.endTime,
},
endTime: {
gt: selectedSlot.startTime,
},
},
});
if (overlappingBooking) {
throw new AdminBookingServiceError(
`El horario seleccionado ya fue reservado para el dia ${formatIsoDate(date)}.`,
409
);
}
const booking = await tx.courtBooking.create({
data: {
id: uuidv7(),
bookingCode: generateBookingCode(),
courtId: court.id,
bookingDate: date,
startTime: selectedSlot.startTime,
endTime: selectedSlot.endTime,
customerName: input.customerName.trim(),
customerPhone: input.customerPhone.trim(),
customerEmail: input.customerEmail?.trim() ?? '',
status: 'CONFIRMED',
recurringGroupId: groupId,
},
include: {
court: {
select: {
id: true,
name: true,
sport: {
select: {
id: true,
name: true,
slug: true,
},
},
complex: {
select: {
id: true,
complexName: true,
},
},
},
},
},
});
createdBookings.push(booking);
}
return { group, bookings: createdBookings };
});
return {
id: result.group.id,
complexId,
courtId: court.id,
startTime: result.group.startTime,
endTime: result.group.endTime,
dayOfWeek: result.group.dayOfWeek,
startDate: formatIsoDate(result.group.startDate),
endDate: result.group.endDate ? formatIsoDate(result.group.endDate) : null,
status: 'ACTIVE' as const,
customerName: result.group.customerName,
customerPhone: result.group.customerPhone,
customerEmail: result.group.customerEmail,
bookings: result.bookings.map((b) => mapBookingResponse(b)),
createdAt: result.group.createdAt.toISOString(),
updatedAt: result.group.updatedAt.toISOString(),
};
} catch (error) {
if (error instanceof AdminBookingServiceError) {
throw error;
}
const prismaError = error as {
code?: string;
meta?: {
target?: string[] | string;
};
};
if (prismaError.code === 'P2002') {
const targets = Array.isArray(prismaError.meta?.target)
? prismaError.meta?.target
: [prismaError.meta?.target];
const isBookingCodeCollision = targets.some((target) =>
String(target).includes('booking_code')
);
if (isBookingCodeCollision) {
continue;
}
throw new AdminBookingServiceError('El horario seleccionado ya fue reservado.', 409);
}
throw error;
}
}
throw new AdminBookingServiceError(
'No se pudo generar un codigo de reserva unico. Intenta nuevamente.',
409
);
}
export async function cancelRecurringGroup(userId: string, groupId: string) {
const group = await db.recurringBookingGroup.findUnique({
where: { id: groupId },
include: {
complex: {
select: {
id: true,
users: {
where: { userId },
select: { userId: true },
},
},
},
},
});
if (!group) {
throw new AdminBookingServiceError('Grupo de reservas no encontrado.', 404);
}
if (group.complex.users.length === 0) {
throw new AdminBookingServiceError('No tienes permisos para administrar este complejo.', 403);
}
if (group.status === 'CANCELLED') {
throw new AdminBookingServiceError('El grupo ya fue cancelado anteriormente.', 409);
}
const now = new Date();
const todayStart = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));
await db.$transaction(async (tx) => {
await tx.recurringBookingGroup.update({
where: { id: groupId },
data: { status: 'CANCELLED' },
});
const futureBookings = await tx.courtBooking.findMany({
where: {
recurringGroupId: groupId,
bookingDate: { gte: todayStart },
status: 'CONFIRMED',
},
});
for (const booking of futureBookings) {
await tx.courtBookingLog.create({
data: {
id: uuidv7(),
bookingCode: booking.bookingCode,
courtId: booking.courtId,
bookingDate: booking.bookingDate,
startTime: booking.startTime,
endTime: booking.endTime,
customerName: booking.customerName,
customerPhone: booking.customerPhone,
customerEmail: booking.customerEmail,
previousStatus: booking.status,
newStatus: 'CANCELLED',
changedAt: new Date(),
},
});
await tx.courtBooking.delete({
where: { id: booking.id },
});
}
});
return { ok: true };
}
export async function listRecurringGroups(complexId: string): Promise<RecurringBookingGroup[]> {
const groups = await db.recurringBookingGroup.findMany({
where: { complexId, status: 'ACTIVE' },
include: {
court: {
select: {
id: true,
name: true,
sport: {
select: { id: true, name: true, slug: true },
},
complex: {
select: { id: true, complexName: true },
},
},
},
bookings: {
orderBy: { bookingDate: 'asc' },
include: {
court: {
select: {
id: true,
name: true,
sport: {
select: { id: true, name: true, slug: true },
},
complex: {
select: { id: true, complexName: true },
},
},
},
},
},
},
orderBy: { createdAt: 'desc' },
});
return groups.map((group) => ({
id: group.id,
complexId: group.complexId,
courtId: group.courtId,
startTime: group.startTime,
endTime: group.endTime,
dayOfWeek: group.dayOfWeek,
startDate: formatIsoDate(group.startDate),
endDate: group.endDate ? formatIsoDate(group.endDate) : null,
status: group.status,
customerName: group.customerName,
customerPhone: group.customerPhone,
customerEmail: group.customerEmail,
bookings: group.bookings.map(mapBookingResponse),
createdAt: group.createdAt.toISOString(),
updatedAt: group.updatedAt.toISOString(),
}));
}
export async function updateRecurringGroup(
userId: string,
groupId: string,
input: UpdateRecurringGroupInput
): Promise<RecurringBookingGroup> {
const group = await db.recurringBookingGroup.findUnique({
where: { id: groupId },
include: {
complex: {
select: {
id: true,
users: {
where: { userId },
select: { userId: true },
},
},
},
},
});
if (!group) {
throw new AdminBookingServiceError('Grupo de turnos fijos no encontrado.', 404);
}
if (group.complex.users.length === 0) {
throw new AdminBookingServiceError('No tienes permisos para administrar este complejo.', 403);
}
if (group.status === 'CANCELLED') {
throw new AdminBookingServiceError('No se puede editar un grupo cancelado.', 409);
}
const courtId = input.courtId ?? group.courtId;
const dayOfWeek = input.dayOfWeek ?? group.dayOfWeek;
const startTime = input.startTime ?? group.startTime;
const scheduleChanged =
input.courtId !== undefined || input.dayOfWeek !== undefined || input.startTime !== undefined;
if (scheduleChanged) {
const court = await db.court.findFirst({
where: { id: courtId, complexId: group.complexId },
include: {
availabilities: {
where: { dayOfWeek: dayOfWeek as DayOfWeek },
orderBy: { startTime: 'asc' },
},
},
});
if (!court) {
throw new AdminBookingServiceError('La cancha seleccionada no existe en el complejo.', 404);
}
const validSlots = buildSlots(court.availabilities, court.slotDurationMinutes);
const selectedEndMinutes = toMinutes(startTime) + court.slotDurationMinutes;
const endTime = minutesToTime(selectedEndMinutes);
const slotExists = validSlots.some(
(slot) => slot.startTime === startTime && slot.endTime === endTime
);
if (!slotExists) {
throw new AdminBookingServiceError(
'El horario seleccionado no esta disponible para esa cancha.',
409
);
}
const now = new Date();
const todayStart = new Date(
Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())
);
await db.$transaction(async (tx) => {
await tx.recurringBookingGroup.update({
where: { id: groupId },
data: {
courtId,
dayOfWeek: dayOfWeek as DayOfWeek,
startTime,
endTime,
customerName: input.customerName?.trim() ?? group.customerName,
customerPhone: input.customerPhone?.trim() ?? group.customerPhone,
customerEmail: input.customerEmail?.trim() ?? group.customerEmail,
},
});
const futureBookings = await tx.courtBooking.findMany({
where: {
recurringGroupId: groupId,
bookingDate: { gte: todayStart },
status: 'CONFIRMED',
},
});
for (const booking of futureBookings) {
await tx.courtBookingLog.create({
data: {
id: uuidv7(),
bookingCode: booking.bookingCode,
courtId: booking.courtId,
bookingDate: booking.bookingDate,
startTime: booking.startTime,
endTime: booking.endTime,
customerName: booking.customerName,
customerPhone: booking.customerPhone,
customerEmail: booking.customerEmail,
previousStatus: booking.status,
newStatus: 'CANCELLED',
changedAt: new Date(),
},
});
await tx.courtBooking.delete({
where: { id: booking.id },
});
}
const dayIndex = DAY_INDEX_BY_VALUE[dayOfWeek] ?? 0;
const recurringDates = Array.from(
generateRecurringDates(todayStart, group.endDate, dayIndex)
);
for (const date of recurringDates) {
const overlappingBooking = await tx.courtBooking.findFirst({
where: {
courtId,
bookingDate: date,
status: 'CONFIRMED',
startTime: { lt: endTime },
endTime: { gt: startTime },
},
});
if (overlappingBooking) {
throw new AdminBookingServiceError(
`El horario seleccionado ya fue reservado para el dia ${formatIsoDate(date)}.`,
409
);
}
await tx.courtBooking.create({
data: {
id: uuidv7(),
bookingCode: generateBookingCode(),
courtId,
bookingDate: date,
startTime,
endTime,
customerName: input.customerName?.trim() ?? group.customerName,
customerPhone: input.customerPhone?.trim() ?? group.customerPhone,
customerEmail: input.customerEmail?.trim() ?? group.customerEmail,
status: 'CONFIRMED',
recurringGroupId: groupId,
},
include: {
court: {
select: {
id: true,
name: true,
sport: { select: { id: true, name: true, slug: true } },
complex: { select: { id: true, complexName: true } },
},
},
},
});
}
});
} else {
await db.recurringBookingGroup.update({
where: { id: groupId },
data: {
...(input.customerName !== undefined && { customerName: input.customerName.trim() }),
...(input.customerPhone !== undefined && { customerPhone: input.customerPhone.trim() }),
...(input.customerEmail !== undefined && { customerEmail: input.customerEmail.trim() }),
},
});
const now = new Date();
const todayStart = new Date(
Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())
);
const updateData: Record<string, string> = {};
if (input.customerName !== undefined) updateData.customerName = input.customerName.trim();
if (input.customerPhone !== undefined) updateData.customerPhone = input.customerPhone.trim();
if (input.customerEmail !== undefined) updateData.customerEmail = input.customerEmail.trim();
if (Object.keys(updateData).length > 0) {
await db.courtBooking.updateMany({
where: { recurringGroupId: groupId, bookingDate: { gte: todayStart } },
data: updateData,
});
}
}
const updated = await db.recurringBookingGroup.findUnique({
where: { id: groupId },
include: {
court: {
select: {
id: true,
name: true,
sport: { select: { id: true, name: true, slug: true } },
complex: { select: { id: true, complexName: true } },
},
},
bookings: {
orderBy: { bookingDate: 'asc' },
include: {
court: {
select: {
id: true,
name: true,
sport: { select: { id: true, name: true, slug: true } },
complex: { select: { id: true, complexName: true } },
},
},
},
},
},
});
if (!updated) {
throw new AdminBookingServiceError('Error al actualizar el grupo.', 409);
}
return {
id: updated.id,
complexId: updated.complexId,
courtId: updated.courtId,
startTime: updated.startTime,
endTime: updated.endTime,
dayOfWeek: updated.dayOfWeek,
startDate: formatIsoDate(updated.startDate),
endDate: updated.endDate ? formatIsoDate(updated.endDate) : null,
status: updated.status,
customerName: updated.customerName,
customerPhone: updated.customerPhone,
customerEmail: updated.customerEmail,
bookings: updated.bookings.map(mapBookingResponse),
createdAt: updated.createdAt.toISOString(),
updatedAt: updated.updatedAt.toISOString(),
};
}

View File

@@ -0,0 +1,67 @@
import { validate } from '@/lib/http/validate';
import { requireAuth } from '@/middlewares/require-auth.middleware';
import { requireSuperAdmin } from '@/middlewares/require-super-admin.middleware';
import { blockUserHandler } from '@/modules/admin/handlers/block-user.handler';
import { createPlanHandler } from '@/modules/admin/handlers/create-plan.handler';
import { deletePlanHandler } from '@/modules/admin/handlers/delete-plan.handler';
import { getGeoStatsHandler } from '@/modules/admin/handlers/get-geo-stats.handler';
import { getUserSessionsHandler } from '@/modules/admin/handlers/get-user-sessions.handler';
import { listComplexesHandler } from '@/modules/admin/handlers/list-complexes.handler';
import { listPlansAdminHandler } from '@/modules/admin/handlers/list-plans-admin.handler';
import { listUsersHandler } from '@/modules/admin/handlers/list-users.handler';
import { revokeAllSessionsHandler } from '@/modules/admin/handlers/revoke-all-sessions.handler';
import { unblockUserHandler } from '@/modules/admin/handlers/unblock-user.handler';
import { updatePlanHandler } from '@/modules/admin/handlers/update-plan.handler';
import type { AppEnv } from '@/types/hono';
import {
adminBlockUserSchema,
adminCreatePlanSchema,
adminUpdatePlanSchema,
} from '@repo/api-contract';
import { Hono } from 'hono';
import { z } from 'zod';
export const adminRoutes = new Hono<AppEnv>();
adminRoutes.use('*', requireAuth, requireSuperAdmin);
adminRoutes.get('/complexes', listComplexesHandler);
adminRoutes.get('/plans', listPlansAdminHandler);
adminRoutes.post('/plans', validate.json(adminCreatePlanSchema), createPlanHandler);
adminRoutes.patch(
'/plans/:code',
validate.param(z.object({ code: z.string().min(1).max(10) })),
validate.json(adminUpdatePlanSchema),
updatePlanHandler
);
adminRoutes.delete(
'/plans/:code',
validate.param(z.object({ code: z.string().min(1).max(10) })),
deletePlanHandler
);
adminRoutes.get('/geo-stats', getGeoStatsHandler);
adminRoutes.get('/users', listUsersHandler);
adminRoutes.post(
'/users/:id/block',
validate.param(z.object({ id: z.string().min(1) })),
validate.json(adminBlockUserSchema),
blockUserHandler
);
adminRoutes.post(
'/users/:id/unblock',
validate.param(z.object({ id: z.string().min(1) })),
unblockUserHandler
);
adminRoutes.get(
'/users/:id/sessions',
validate.param(z.object({ id: z.string().min(1) })),
getUserSessionsHandler
);
adminRoutes.post(
'/users/:id/sessions/revoke-all',
validate.param(z.object({ id: z.string().min(1) })),
revokeAllSessionsHandler
);

View File

@@ -0,0 +1,20 @@
import { AdminServiceError, blockUser } from '@/modules/admin/services/admin-users.service';
import type { AppContext } from '@/types/hono';
import type { AdminBlockUserInput } from '@repo/api-contract';
type BlockUserParams = { id: string };
export async function blockUserHandler(c: AppContext) {
const { id } = c.req.valid('param' as never) as BlockUserParams;
const body = c.req.valid('json' as never) as AdminBlockUserInput;
try {
await blockUser(id, body.banReason);
return c.json({ message: 'Usuario bloqueado correctamente.' });
} catch (error) {
if (error instanceof AdminServiceError) {
return c.json({ message: error.message }, error.status);
}
throw error;
}
}

View File

@@ -0,0 +1,34 @@
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
);
}

View File

@@ -0,0 +1,31 @@
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.' });
}

View File

@@ -0,0 +1,8 @@
import { getGeoStats } from '@/modules/admin/services/admin-geo.service';
import type { AppEnv } from '@/types/hono';
import type { Handler } from 'hono';
export const getGeoStatsHandler: Handler<AppEnv> = async (c) => {
const stats = await getGeoStats();
return c.json(stats);
};

View File

@@ -0,0 +1,12 @@
import { getUserSessions } from '@/modules/admin/services/admin-users.service';
import type { AppContext } from '@/types/hono';
type UserSessionsParams = { id: string };
export async function getUserSessionsHandler(c: AppContext) {
const { id } = c.req.valid('param' as never) as UserSessionsParams;
const sessions = await getUserSessions(id);
return c.json(sessions);
}

View File

@@ -0,0 +1,7 @@
import { getComplexStatsList } from '@/modules/admin/services/complex-stats.service';
import type { AppContext } from '@/types/hono';
export async function listComplexesHandler(c: AppContext) {
const stats = await getComplexStatsList();
return c.json(stats);
}

View File

@@ -0,0 +1,24 @@
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,
}))
);
}

View File

@@ -0,0 +1,8 @@
import { listAdminUsers } from '@/modules/admin/services/admin-users.service';
import type { AppContext } from '@/types/hono';
export async function listUsersHandler(c: AppContext) {
const search = c.req.query('search');
const users = await listAdminUsers(search);
return c.json(users);
}

View File

@@ -0,0 +1,12 @@
import { revokeAllUserSessions } from '@/modules/admin/services/admin-users.service';
import type { AppContext } from '@/types/hono';
type RevokeSessionsParams = { id: string };
export async function revokeAllSessionsHandler(c: AppContext) {
const { id } = c.req.valid('param' as never) as RevokeSessionsParams;
const count = await revokeAllUserSessions(id);
return c.json({ message: `${count} sesiones cerradas correctamente.` });
}

View File

@@ -0,0 +1,12 @@
import { unblockUser } from '@/modules/admin/services/admin-users.service';
import type { AppContext } from '@/types/hono';
type UnblockUserParams = { id: string };
export async function unblockUserHandler(c: AppContext) {
const { id } = c.req.valid('param' as never) as UnblockUserParams;
await unblockUser(id);
return c.json({ message: 'Usuario desbloqueado correctamente.' });
}

View File

@@ -0,0 +1,35 @@
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,
});
}

View File

@@ -0,0 +1,67 @@
import { db } from '@/lib/prisma';
type GeoStatsCountry = {
country: string;
countryCode: string;
totalUsers: number;
cities: { city: string; userCount: number }[];
};
export async function getGeoStats(): Promise<{
countries: GeoStatsCountry[];
totalUniqueCountries: number;
}> {
const sessions = await db.session.findMany({
where: {
country: { not: null },
expiresAt: { gt: new Date() },
},
select: {
country: true,
countryCode: true,
city: true,
userId: true,
},
});
const countryMap = new Map<
string,
{ country: string; countryCode: string; users: Set<string>; cities: Map<string, Set<string>> }
>();
for (const s of sessions) {
const code = s.countryCode || 'XX';
let entry = countryMap.get(code);
if (!entry) {
entry = {
country: s.country || 'Desconocido',
countryCode: code,
users: new Set(),
cities: new Map(),
};
countryMap.set(code, entry);
}
entry.users.add(s.userId);
const cityName = s.city || 'Desconocida';
let cityUsers = entry.cities.get(cityName);
if (!cityUsers) {
cityUsers = new Set();
entry.cities.set(cityName, cityUsers);
}
cityUsers.add(s.userId);
}
const countries = Array.from(countryMap.values())
.map((entry) => ({
country: entry.country,
countryCode: entry.countryCode,
totalUsers: entry.users.size,
cities: Array.from(entry.cities.entries())
.map(([city, users]) => ({ city, userCount: users.size }))
.sort((a, b) => b.userCount - a.userCount),
}))
.sort((a, b) => b.totalUsers - a.totalUsers);
return { countries, totalUniqueCountries: countries.length };
}

View File

@@ -0,0 +1,146 @@
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;
}

View File

@@ -0,0 +1,80 @@
import { db } from '@/lib/prisma';
type ComplexStats = {
id: string;
complexName: string;
complexSlug: string;
city: string | null;
planCode: string | null;
planName: string | null;
userCount: number;
courtCount: number;
totalBookings: number;
avgBookingsPerDay: number;
bookingsByStatus: {
confirmed: number;
cancelled: number;
completed: number;
noshow: number;
};
paymentStatus: 'active' | 'no_plan' | 'expired';
};
export async function getComplexStatsList(): Promise<ComplexStats[]> {
const complexes = await db.complex.findMany({
include: {
plan: true,
users: true,
courts: {
include: {
bookings: true,
},
},
},
});
return complexes.map((complex) => {
const courtCount = complex.courts.length;
const allBookings = complex.courts.flatMap((c) => c.bookings);
const bookingsByStatus = {
confirmed: allBookings.filter((b) => b.status === 'CONFIRMED').length,
cancelled: allBookings.filter((b) => b.status === 'CANCELLED').length,
completed: allBookings.filter((b) => b.status === 'COMPLETED').length,
noshow: allBookings.filter((b) => b.status === 'NOSHOW').length,
};
const totalBookings = allBookings.length;
let avgBookingsPerDay = 0;
if (allBookings.length > 0) {
const dates = allBookings.map((b) => b.bookingDate);
const minDate = new Date(Math.min(...dates.map((d) => d.getTime())));
const daysDiff = Math.max(
1,
Math.ceil((Date.now() - minDate.getTime()) / (1000 * 60 * 60 * 24))
);
avgBookingsPerDay = Math.round((totalBookings / daysDiff) * 100) / 100;
}
let paymentStatus: 'active' | 'no_plan' | 'expired' = 'no_plan';
if (complex.plan) {
paymentStatus = 'active';
}
return {
id: complex.id,
complexName: complex.complexName,
complexSlug: complex.complexSlug,
city: complex.city,
planCode: complex.planCode,
planName: complex.plan?.name ?? null,
userCount: complex.users.length,
courtCount,
totalBookings,
avgBookingsPerDay,
bookingsByStatus,
paymentStatus,
};
});
}

View File

@@ -1,5 +1,6 @@
import { Prisma } from '@/generated/prisma/client'; import { Prisma } from '@/generated/prisma/client';
import { db } from '@/lib/prisma'; import { db } from '@/lib/prisma';
import { parsePlanRules } from '@/modules/plan/services/plan-rules.service';
import { v7 as uuidv7 } from 'uuid'; import { v7 as uuidv7 } from 'uuid';
export type CreateComplexInput = { export type CreateComplexInput = {
@@ -185,7 +186,11 @@ export async function listMyComplexes(userId: string) {
const complexUsers = await db.complexUser.findMany({ const complexUsers = await db.complexUser.findMany({
where: { userId }, where: { userId },
include: { include: {
complex: true, complex: {
include: {
plan: true,
},
},
}, },
orderBy: { orderBy: {
createdAt: 'asc', createdAt: 'asc',
@@ -195,6 +200,9 @@ export async function listMyComplexes(userId: string) {
return complexUsers.map((complexUser) => ({ return complexUsers.map((complexUser) => ({
...complexUser.complex, ...complexUser.complex,
role: complexUser.role, role: complexUser.role,
planFeatures: complexUser.complex.plan
? parsePlanRules(complexUser.complex.plan.rules).features
: null,
})); }));
} }
@@ -207,7 +215,11 @@ export async function getCurrentComplex(userId: string, complexId: string) {
}, },
}, },
include: { include: {
complex: true, complex: {
include: {
plan: true,
},
},
}, },
}); });
@@ -216,6 +228,9 @@ export async function getCurrentComplex(userId: string, complexId: string) {
return { return {
...complexUser.complex, ...complexUser.complex,
role: complexUser.role, role: complexUser.role,
planFeatures: complexUser.complex.plan
? parsePlanRules(complexUser.complex.plan.rules).features
: null,
}; };
} }
@@ -228,7 +243,11 @@ export async function selectComplex(userId: string, complexId: string) {
}, },
}, },
include: { include: {
complex: true, complex: {
include: {
plan: true,
},
},
}, },
}); });
@@ -237,6 +256,9 @@ export async function selectComplex(userId: string, complexId: string) {
return { return {
...complexUser.complex, ...complexUser.complex,
role: complexUser.role, role: complexUser.role,
planFeatures: complexUser.complex.plan
? parsePlanRules(complexUser.complex.plan.rules).features
: null,
}; };
} }

View File

@@ -1,5 +1,6 @@
import { createHash, randomInt } from 'node:crypto'; import { createHash, randomInt } from 'node:crypto';
import { auth } from '@/lib/auth'; import { auth } from '@/lib/auth';
import { type IpGeoInfo, fetchGeoInfo } from '@/lib/geoip';
import { sendMail } from '@/lib/mailer'; import { sendMail } from '@/lib/mailer';
import { db } from '@/lib/prisma'; import { db } from '@/lib/prisma';
import type { import type {
@@ -82,13 +83,6 @@ type UserAgentInfo = {
device: string; device: string;
}; };
type IpGeoInfo = {
ip: string;
city: string;
country: string;
countryCode: string;
};
function parseUserAgent(ua: string | undefined): UserAgentInfo { function parseUserAgent(ua: string | undefined): UserAgentInfo {
if (!ua) { if (!ua) {
return { browser: 'Desconocido', os: 'Desconocido', device: 'Desconocido' }; return { browser: 'Desconocido', os: 'Desconocido', device: 'Desconocido' };
@@ -118,31 +112,6 @@ function parseUserAgent(ua: string | undefined): UserAgentInfo {
return { browser, os, device }; return { browser, os, device };
} }
async function fetchGeoInfo(ip: string): Promise<IpGeoInfo | null> {
if (ip === '127.0.0.1' || ip === '::1' || ip.startsWith('192.168.') || ip.startsWith('10.')) {
return { ip, city: 'Red local', country: 'Red local', countryCode: 'LOCAL' };
}
try {
const response = await fetch(
`http://ip-api.com/json/${ip}?fields=status,country,countryCode,city,query`
);
if (!response.ok) return null;
const data = await response.json();
if (data.status !== 'success') return null;
return {
ip: data.query,
city: data.city || 'Desconocida',
country: data.country || 'Desconocido',
countryCode: data.countryCode || '',
};
} catch {
return null;
}
}
async function sendResetOtpEmail(email: string, otpCode: string) { async function sendResetOtpEmail(email: string, otpCode: string) {
await sendMail({ await sendMail({
to: email, to: email,

View File

@@ -1,4 +1,6 @@
import { fetchGeoInfo } from '@/lib/geoip';
import { db } from '@/lib/prisma'; import { db } from '@/lib/prisma';
import { resolvePlanPrice } from '@/modules/plan/services/plan-pricing.service';
import { parsePlanRules } from '@/modules/plan/services/plan-rules.service'; import { parsePlanRules } from '@/modules/plan/services/plan-rules.service';
import type { AppContext } from '@/types/hono'; import type { AppContext } from '@/types/hono';
@@ -9,13 +11,26 @@ export async function listPlansHandler(c: AppContext) {
}, },
}); });
const countryParam = c.req.query('country');
let countryCode: string | undefined;
if (countryParam) {
countryCode = countryParam;
} else {
const ip = c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || '127.0.0.1';
const geo = await fetchGeoInfo(ip);
countryCode = geo?.countryCode || undefined;
}
return c.json( return c.json(
plans.map((plan) => { plans.map((plan) => {
const rules = parsePlanRules(plan.rules); const rules = parsePlanRules(plan.rules);
const resolved = resolvePlanPrice(Number(plan.price), rules, countryCode);
return { return {
code: plan.code, code: plan.code,
name: plan.name, name: plan.name,
price: Number(plan.price), price: resolved.amount,
currency: resolved.currency,
features: rules.features, features: rules.features,
limits: rules.limits, limits: rules.limits,
}; };

View File

@@ -0,0 +1,25 @@
import type { PlanRules } from '@repo/api-contract';
export type PlanPrice = {
amount: number;
currency: string;
};
export function resolvePlanPrice(
defaultPrice: number,
rules: PlanRules,
countryCode?: string
): PlanPrice {
if (!countryCode) {
return { amount: defaultPrice, currency: 'USD' };
}
if (rules.version === 'v2' && rules.pricing?.overrides) {
const override = rules.pricing.overrides[countryCode];
if (override) {
return { amount: override.amount, currency: override.currency };
}
}
return { amount: defaultPrice, currency: 'USD' };
}

View File

@@ -770,7 +770,7 @@ export async function cancelPublicBooking(complexSlug: string, input: CancelPubl
} }
if (booking.customerPhone !== input.customerPhone.trim()) { if (booking.customerPhone !== input.customerPhone.trim()) {
throw new PublicBookingServiceError('Los datos ingresados no coinciden con la reserva.', 403); throw new PublicBookingServiceError('Los datos ingresados no coinciden con la reserva.', 400);
} }
const date = formatIsoDate(booking.bookingDate); const date = formatIsoDate(booking.bookingDate);

View File

@@ -1,5 +1,6 @@
import path from 'node:path'; import path from 'node:path';
import { adminBookingRoutes } from '@/modules/admin-booking/admin-booking.routes'; import { adminBookingRoutes } from '@/modules/admin-booking/admin-booking.routes';
import { adminRoutes } from '@/modules/admin/admin.routes';
import { authRoutes } from '@/modules/auth/auth.routes'; import { authRoutes } from '@/modules/auth/auth.routes';
import { complexRoutes } from '@/modules/complex/complex.routes'; import { complexRoutes } from '@/modules/complex/complex.routes';
import { courtRoutes } from '@/modules/court/court.routes'; import { courtRoutes } from '@/modules/court/court.routes';
@@ -17,6 +18,7 @@ import { healthCheckRoutes } from './modules/health-check/health-check.routes';
export function registerRoutes(app: Hono<AppEnv>) { export function registerRoutes(app: Hono<AppEnv>) {
app app
.route('', authRoutes) .route('', authRoutes)
.route('/api/admin', adminRoutes)
.route('/api/user', userRoutes) .route('/api/user', userRoutes)
.route('/api/plans', planRoutes) .route('/api/plans', planRoutes)
.route('/api/password-reset', passwordResetRoutes) .route('/api/password-reset', passwordResetRoutes)

View File

@@ -0,0 +1,89 @@
import { expect, test } from 'bun:test';
import type { PlanRules } from '@repo/api-contract';
const DEFAULT_PRICE = 29.99;
function makeV1Rules(): PlanRules {
return {
version: 'v1',
limits: { maxCourts: 3, maxBookingsPerDay: 50 },
policies: { allowOverbooking: false },
features: {
onlinePayments: false,
publicBookingPage: false,
advancedReports: false,
whatsappReminders: false,
fixedSlots: false,
},
};
}
function makeV2Rules(overrides?: Record<string, { amount: number; currency: string }>): PlanRules {
return {
version: 'v2',
limits: { maxCourts: 5, maxBookingsPerDay: 100 },
policies: { allowOverbooking: false },
features: {
onlinePayments: true,
publicBookingPage: true,
advancedReports: true,
whatsappReminders: true,
fixedSlots: true,
},
...(overrides ? { pricing: { overrides } } : {}),
};
}
const { resolvePlanPrice } = await import('@/modules/plan/services/plan-pricing.service');
test('returns default price in USD when no country code is provided', () => {
const result = resolvePlanPrice(
DEFAULT_PRICE,
makeV2Rules({ AR: { amount: 14999, currency: 'ARS' } })
);
expect(result).toEqual({ amount: DEFAULT_PRICE, currency: 'USD' });
});
test('returns overridden price for matching country code', () => {
const rules = makeV2Rules({ AR: { amount: 14999, currency: 'ARS' } });
const result = resolvePlanPrice(DEFAULT_PRICE, rules, 'AR');
expect(result).toEqual({ amount: 14999, currency: 'ARS' });
});
test('returns default price for country without override', () => {
const rules = makeV2Rules({ AR: { amount: 14999, currency: 'ARS' } });
const result = resolvePlanPrice(DEFAULT_PRICE, rules, 'CL');
expect(result).toEqual({ amount: DEFAULT_PRICE, currency: 'USD' });
});
test('returns default price for v1 rules (no pricing config)', () => {
const result = resolvePlanPrice(DEFAULT_PRICE, makeV1Rules(), 'AR');
expect(result).toEqual({ amount: DEFAULT_PRICE, currency: 'USD' });
});
test('returns default price for v2 rules with empty overrides', () => {
const rules = makeV2Rules({});
const result = resolvePlanPrice(DEFAULT_PRICE, rules, 'AR');
expect(result).toEqual({ amount: DEFAULT_PRICE, currency: 'USD' });
});
test('returns default price when v2 rules has no pricing section', () => {
const rules = makeV2Rules();
const result = resolvePlanPrice(DEFAULT_PRICE, rules, 'AR');
expect(result).toEqual({ amount: DEFAULT_PRICE, currency: 'USD' });
});
test('selects correct override among multiple countries', () => {
const rules = makeV2Rules({
AR: { amount: 24999, currency: 'ARS' },
BR: { amount: 59.99, currency: 'USD' },
});
expect(resolvePlanPrice(DEFAULT_PRICE, rules, 'AR')).toEqual({ amount: 24999, currency: 'ARS' });
expect(resolvePlanPrice(DEFAULT_PRICE, rules, 'BR')).toEqual({ amount: 59.99, currency: 'USD' });
expect(resolvePlanPrice(DEFAULT_PRICE, rules, 'US')).toEqual({
amount: DEFAULT_PRICE,
currency: 'USD',
});
});

View File

@@ -6,6 +6,7 @@ import {
Menu, Menu,
Moon, Moon,
Settings, Settings,
Shield,
Sun, Sun,
User, User,
X, X,
@@ -241,6 +242,18 @@ export function Header() {
</DropdownMenuItem> </DropdownMenuItem>
)} )}
{user?.role === 'super_admin' && (
<DropdownMenuItem
className="cursor-pointer rounded-xl"
onSelect={() => {
void navigate({ to: '/admin' });
}}
>
<Shield className="mr-2 size-4 text-emerald-600" />
Admin
</DropdownMenuItem>
)}
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem <DropdownMenuItem
@@ -405,6 +418,20 @@ export function Header() {
</Button> </Button>
)} )}
{user?.role === 'super_admin' && (
<Button
variant="ghost"
className="justify-start rounded-2xl"
onClick={() => {
setOpen(false);
void navigate({ to: '/admin' });
}}
>
<Shield className="mr-2 size-4 text-emerald-600" />
Admin
</Button>
)}
{isAuthenticated && ( {isAuthenticated && (
<Button <Button
variant="ghost" variant="ghost"

View File

@@ -0,0 +1,31 @@
import { type VariantProps, cva } from 'class-variance-authority';
import type { HTMLAttributes } from 'react';
import { cn } from '@/lib/utils';
const badgeVariants = cva(
'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
{
variants: {
variant: {
default: 'border-transparent bg-primary text-primary-foreground shadow-sm',
secondary: 'border-transparent bg-secondary text-secondary-foreground',
destructive: 'border-transparent bg-destructive text-destructive-foreground shadow-sm',
outline: 'text-foreground',
},
},
defaultVariants: {
variant: 'default',
},
}
);
export interface BadgeProps
extends HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return <div className={cn(badgeVariants({ variant }), className)} {...props} />;
}
export { Badge, badgeVariants };

View File

@@ -0,0 +1,68 @@
import { Button } from '@/components/ui/button';
import { Link } from '@tanstack/react-router';
import { BarChart3, Building2, Home, LayoutDashboard, Shield, Users } from 'lucide-react';
import type { ReactNode } from 'react';
type NavItem = {
label: string;
href: string;
icon: ReactNode;
};
const navItems: NavItem[] = [
{ label: 'Dashboard', href: '/admin', icon: <LayoutDashboard className="size-4" /> },
{ label: 'Complejos', href: '/admin/complexes', icon: <Building2 className="size-4" /> },
{ label: 'Planes', href: '/admin/plans', icon: <BarChart3 className="size-4" /> },
{ label: 'Usuarios', href: '/admin/users', icon: <Users className="size-4" /> },
];
interface AdminLayoutProps {
children: ReactNode;
currentPath: string;
}
export function AdminLayout({ children, currentPath }: AdminLayoutProps) {
return (
<div className="flex min-h-[calc(100dvh-4rem)] gap-0">
<aside className="hidden w-56 shrink-0 border-r border-border/60 md:block">
<nav className="sticky top-20 flex flex-col gap-1 p-4">
<div className="mb-4 flex items-center gap-2 px-3">
<Shield className="size-4 text-emerald-600" />
<span className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
Admin
</span>
</div>
{navItems.map((item) => {
const isActive = currentPath === item.href;
return (
<Button
key={item.href}
variant={isActive ? 'secondary' : 'ghost'}
className="justify-start gap-3 rounded-xl"
asChild
>
<Link to={item.href}>
{item.icon}
{item.label}
</Link>
</Button>
);
})}
<div className="mt-6 border-t border-border/60 pt-4">
<Button variant="ghost" className="w-full justify-start gap-3 rounded-xl" asChild>
<Link to="/">
<Home className="size-4" />
Volver a Playzer
</Link>
</Button>
</div>
</nav>
</aside>
<div className="flex-1 overflow-hidden p-4 sm:p-6 lg:p-8">{children}</div>
</div>
);
}

View File

@@ -0,0 +1,254 @@
import { apiClient } from '@/lib/api-client';
import { useQuery } from '@tanstack/react-query';
import { Link, useLocation } from '@tanstack/react-router';
import {
Building2,
CalendarDays,
ChevronDown,
ChevronRight,
Crosshair,
Globe,
Users,
} from 'lucide-react';
import { Fragment, useState } from 'react';
import { AdminLayout } from './admin-layout';
function countryFlag(countryCode: string): string {
if (countryCode === 'LOCAL' || countryCode === 'XX') return '';
const codePoints = countryCode
.toUpperCase()
.split('')
.map((c) => 0x1f1e6 + c.charCodeAt(0) - 65);
return String.fromCodePoint(...codePoints);
}
export function AdminPage() {
const location = useLocation();
const [expandedCountry, setExpandedCountry] = useState<string | null>(null);
const complexesQuery = useQuery({
queryKey: ['admin-complexes'],
queryFn: () => apiClient.admin.listComplexes(),
});
const usersQuery = useQuery({
queryKey: ['admin-users'],
queryFn: () => apiClient.admin.listUsers(),
});
const geoQuery = useQuery({
queryKey: ['admin-geo-stats'],
queryFn: () => apiClient.admin.getGeoStats(),
});
const stats = {
totalComplexes: complexesQuery.data?.length ?? 0,
totalUsers: usersQuery.data?.length ?? 0,
totalCourts: complexesQuery.data?.reduce((sum, c) => sum + c.courtCount, 0) ?? 0,
totalBookings:
complexesQuery.data?.reduce((sum, c) => sum + Math.round(c.avgBookingsPerDay * 30), 0) ?? 0,
};
const cards = [
{
label: 'Complejos',
value: stats.totalComplexes,
icon: Building2,
color: 'text-blue-600',
bg: 'bg-blue-100 dark:bg-blue-900/30',
},
{
label: 'Usuarios',
value: stats.totalUsers,
icon: Users,
color: 'text-emerald-600',
bg: 'bg-emerald-100 dark:bg-emerald-900/30',
},
{
label: 'Canchas',
value: stats.totalCourts,
icon: Crosshair,
color: 'text-purple-600',
bg: 'bg-purple-100 dark:bg-purple-900/30',
},
{
label: 'Reservas (30d)',
value: stats.totalBookings.toLocaleString(),
icon: CalendarDays,
color: 'text-amber-600',
bg: 'bg-amber-100 dark:bg-amber-900/30',
},
];
return (
<AdminLayout currentPath={location.pathname}>
<div className="mb-8">
<h1 className="text-2xl font-bold tracking-tight">Panel de Administración</h1>
<p className="mt-1 text-sm text-muted-foreground">
Gestioná complejos, planes y usuarios de Playzer.
</p>
</div>
<div className="mb-8 grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
{cards.map((card) => {
const Icon = card.icon;
return (
<div
key={card.label}
className="rounded-2xl border border-border/60 bg-card p-5 shadow-sm"
>
<div className="flex items-center gap-3">
<div className={`rounded-xl p-2.5 ${card.bg}`}>
<Icon className={`size-5 ${card.color}`} />
</div>
<div>
<p className="text-xs font-medium uppercase tracking-wider text-muted-foreground">
{card.label}
</p>
<p className="text-2xl font-bold">{card.value}</p>
</div>
</div>
</div>
);
})}
</div>
<div className="mb-6">
<h2 className="mb-3 text-lg font-semibold">Acceso rápido</h2>
<div className="grid gap-3 sm:grid-cols-3">
<Link
to="/admin/complexes"
className="rounded-2xl border border-border/60 bg-card p-4 shadow-sm transition-colors hover:bg-accent"
>
<Building2 className="mb-2 size-5 text-blue-600" />
<p className="font-medium">Complejos</p>
<p className="text-xs text-muted-foreground">
{stats.totalComplexes} complejos registrados
</p>
</Link>
<Link
to="/admin/plans"
className="rounded-2xl border border-border/60 bg-card p-4 shadow-sm transition-colors hover:bg-accent"
>
<CalendarDays className="mb-2 size-5 text-emerald-600" />
<p className="font-medium">Planes</p>
<p className="text-xs text-muted-foreground">Administrar suscripciones</p>
</Link>
<Link
to="/admin/users"
className="rounded-2xl border border-border/60 bg-card p-4 shadow-sm transition-colors hover:bg-accent"
>
<Users className="mb-2 size-5 text-purple-600" />
<p className="font-medium">Usuarios</p>
<p className="text-xs text-muted-foreground">{stats.totalUsers} usuarios registrados</p>
</Link>
</div>
</div>
<div>
<h2 className="mb-3 text-lg font-semibold">Usuarios por país / ciudad</h2>
{geoQuery.isLoading && <p className="text-sm text-muted-foreground">Cargando...</p>}
{geoQuery.data && geoQuery.data.countries.length === 0 && (
<div className="rounded-2xl border border-border/60 bg-card p-8 text-center shadow-sm">
<Globe className="mx-auto mb-2 size-8 text-muted-foreground" />
<p className="text-sm text-muted-foreground">
Aún no hay datos de geolocalización. Aparecerán a medida que los usuarios inicien
sesión.
</p>
</div>
)}
{geoQuery.data && geoQuery.data.countries.length > 0 && (
<div className="overflow-hidden rounded-2xl border border-border/60 bg-card shadow-sm">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-border/60">
<th className="w-8 px-4 py-3" />
<th className="px-4 py-3 text-left font-medium text-muted-foreground">País</th>
<th className="px-4 py-3 text-right font-medium text-muted-foreground">
Usuarios
</th>
</tr>
</thead>
<tbody>
{geoQuery.data.countries.map((country) => {
const isExpanded = expandedCountry === country.countryCode;
const flag = countryFlag(country.countryCode);
return (
<Fragment key={country.countryCode}>
<tr
className={`cursor-pointer border-b border-border/40 transition-colors hover:bg-accent/50 ${isExpanded ? 'bg-accent/30' : ''}`}
onClick={() => setExpandedCountry(isExpanded ? null : country.countryCode)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
setExpandedCountry(isExpanded ? null : country.countryCode);
}
}}
tabIndex={0}
role="button"
>
<td className="px-4 py-3">
{country.cities.length > 1 ||
country.cities[0]?.city !== country.country ? (
<button type="button" className="text-muted-foreground">
{isExpanded ? (
<ChevronDown className="size-4" />
) : (
<ChevronRight className="size-4" />
)}
</button>
) : (
<span className="inline-block w-4" />
)}
</td>
<td className="px-4 py-3">
<span className="mr-2">{flag}</span>
{country.country}
</td>
<td className="px-4 py-3 text-right font-medium">{country.totalUsers}</td>
</tr>
{isExpanded && (
<tr>
<td colSpan={3} className="bg-accent/20 p-0">
<table className="w-full text-xs">
<tbody>
{country.cities.map((city) => (
<tr key={city.city} className="border-b border-border/20">
<td className="w-8 px-4 py-2" />
<td className="px-4 py-2 text-muted-foreground">{city.city}</td>
<td className="px-4 py-2 text-right font-medium">
{city.userCount}
</td>
</tr>
))}
</tbody>
</table>
</td>
</tr>
)}
</Fragment>
);
})}
</tbody>
</table>
<div className="border-t border-border/40 px-4 py-2 text-xs text-muted-foreground">
{geoQuery.data.totalUniqueCountries}{' '}
{geoQuery.data.totalUniqueCountries === 1 ? 'país' : 'países'} · solo sesiones activas
con geolocalización resuelta
</div>
</div>
)}
</div>
</AdminLayout>
);
}

View File

@@ -0,0 +1,167 @@
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { apiClient } from '@/lib/api-client';
import { useQuery } from '@tanstack/react-query';
import { useLocation } from '@tanstack/react-router';
import { Building2, ChevronDown, ChevronUp, MapPin, RefreshCw } from 'lucide-react';
import { useState } from 'react';
import { AdminLayout } from './admin-layout';
function PaymentBadge({ status }: { status: string }) {
if (status === 'active') {
return (
<Badge
variant="outline"
className="border-emerald-300 bg-emerald-50 text-emerald-700 dark:border-emerald-700 dark:bg-emerald-950 dark:text-emerald-400"
>
Activo
</Badge>
);
}
if (status === 'no_plan') {
return (
<Badge variant="outline" className="border-muted-foreground/30 text-muted-foreground">
Sin plan
</Badge>
);
}
return (
<Badge
variant="outline"
className="border-red-300 bg-red-50 text-red-700 dark:border-red-700 dark:bg-red-950 dark:text-red-400"
>
Expirado
</Badge>
);
}
export function ComplexesPage() {
const location = useLocation();
const [expandedId, setExpandedId] = useState<string | null>(null);
const { data, isLoading, refetch } = useQuery({
queryKey: ['admin-complexes'],
queryFn: () => apiClient.admin.listComplexes(),
});
return (
<AdminLayout currentPath={location.pathname}>
<div className="mb-6 flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold tracking-tight">Complejos</h1>
<p className="mt-1 text-sm text-muted-foreground">
{data?.length ?? 0} complejos registrados en Playzer.
</p>
</div>
<Button variant="outline" size="icon" className="rounded-xl" onClick={() => refetch()}>
<RefreshCw className="size-4" />
</Button>
</div>
{isLoading ? (
<div className="flex items-center justify-center py-20">
<p className="text-sm text-muted-foreground">Cargando complejos...</p>
</div>
) : data?.length === 0 ? (
<div className="flex flex-col items-center justify-center py-20">
<Building2 className="mb-3 size-10 text-muted-foreground/50" />
<p className="text-sm text-muted-foreground">No hay complejos registrados.</p>
</div>
) : (
<div className="space-y-3">
{data?.map((complex) => {
const isExpanded = expandedId === complex.id;
return (
<div
key={complex.id}
className="rounded-2xl border border-border/60 bg-card shadow-sm"
>
<button
type="button"
onClick={() => setExpandedId(isExpanded ? null : complex.id)}
className="flex w-full items-center gap-4 px-5 py-4 text-left transition-colors hover:bg-accent/50"
>
<div className="flex min-w-0 flex-1 items-center gap-3">
<div className="rounded-xl bg-emerald-100 p-2.5 dark:bg-emerald-900/30">
<Building2 className="size-5 text-emerald-600" />
</div>
<div className="min-w-0">
<p className="truncate font-medium">{complex.complexName}</p>
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<span>{complex.complexSlug}</span>
{complex.city && (
<>
<span>·</span>
<MapPin className="size-3" />
<span>{complex.city}</span>
</>
)}
</div>
</div>
</div>
<PaymentBadge status={complex.paymentStatus} />
<div className="hidden items-center gap-4 text-xs text-muted-foreground sm:flex">
<span className="text-center">
<span className="block text-sm font-semibold text-foreground">
{complex.courtCount}
</span>
Canchas
</span>
<span className="text-center">
<span className="block text-sm font-semibold text-foreground">
{complex.userCount}
</span>
Usuarios
</span>
<span className="text-center">
<span className="block text-sm font-semibold text-foreground">
{complex.avgBookingsPerDay.toFixed(1)}
</span>
Res/día
</span>
</div>
{isExpanded ? (
<ChevronUp className="size-4 shrink-0 text-muted-foreground" />
) : (
<ChevronDown className="size-4 shrink-0 text-muted-foreground" />
)}
</button>
{isExpanded && (
<div className="border-t border-border/40 px-5 py-4">
<div className="grid grid-cols-2 gap-4 text-sm sm:grid-cols-4">
<div>
<p className="text-xs text-muted-foreground">Plan</p>
<p className="font-medium">{complex.planName ?? 'Sin plan'}</p>
</div>
<div>
<p className="text-xs text-muted-foreground">Canchas</p>
<p className="font-medium">{complex.courtCount}</p>
</div>
<div>
<p className="text-xs text-muted-foreground">Usuarios</p>
<p className="font-medium">{complex.userCount}</p>
</div>
<div>
<p className="text-xs text-muted-foreground">Prom. reservas/día</p>
<p className="font-medium">{complex.avgBookingsPerDay.toFixed(1)}</p>
</div>
</div>
</div>
)}
</div>
);
})}
</div>
)}
</AdminLayout>
);
}

View File

@@ -0,0 +1,810 @@
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { AdminLayout } from '@/features/admin/admin-layout';
import { ApiClientError, apiClient } from '@/lib/api-client';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useLocation } from '@tanstack/react-router';
import { BarChart3, Pencil, Plus, RefreshCw, Trash2, X } from 'lucide-react';
import { useState } from 'react';
type Plan = {
code: string;
name: string;
price: number;
rules: unknown;
lastUpdatedAt: string;
complexCount: number;
};
type PriceOverride = {
country: string;
amount: string;
currency: string;
};
type PlanFormRules = {
limits: {
maxCourts: number;
maxBookingsPerDay: number;
maxActiveUsers: number | '';
maxSports: number | '';
};
features: {
onlinePayments: boolean;
publicBookingPage: boolean;
advancedReports: boolean;
whatsappReminders: boolean;
fixedSlots: boolean;
};
policies: {
maxAdvanceBookingDays: number | '';
minCancellationNoticeHours: number | '';
allowOverbooking: boolean;
};
};
function initOverrides(rules: unknown): PriceOverride[] {
const r = rules as Record<string, unknown> | null | undefined;
const pricing = r?.pricing as Record<string, unknown> | undefined;
const overrides = pricing?.overrides as
| Record<string, { amount: number; currency: string }>
| undefined;
if (!overrides) return [];
return Object.entries(overrides).map(([country, data]) => ({
country,
amount: String(data.amount),
currency: data.currency,
}));
}
function getDefaultRules(): PlanFormRules {
return {
limits: {
maxCourts: 10,
maxBookingsPerDay: 100,
maxActiveUsers: '',
maxSports: '',
},
features: {
onlinePayments: false,
publicBookingPage: false,
advancedReports: false,
whatsappReminders: false,
fixedSlots: false,
},
policies: {
maxAdvanceBookingDays: '',
minCancellationNoticeHours: '',
allowOverbooking: false,
},
};
}
function initRules(plan?: Plan): PlanFormRules {
if (!plan) return getDefaultRules();
const r = plan.rules as Record<string, unknown> | null | undefined;
const limits = r?.limits as Record<string, unknown> | undefined;
const features = r?.features as Record<string, unknown> | undefined;
const policies = r?.policies as Record<string, unknown> | undefined;
return {
limits: {
maxCourts: (limits?.maxCourts as number) ?? 10,
maxBookingsPerDay: (limits?.maxBookingsPerDay as number) ?? 100,
maxActiveUsers: (limits?.maxActiveUsers as number | undefined) ?? '',
maxSports: (limits?.maxSports as number | undefined) ?? '',
},
features: {
onlinePayments: (features?.onlinePayments as boolean) ?? false,
publicBookingPage: (features?.publicBookingPage as boolean) ?? false,
advancedReports: (features?.advancedReports as boolean) ?? false,
whatsappReminders: (features?.whatsappReminders as boolean) ?? false,
fixedSlots: (features?.fixedSlots as boolean) ?? false,
},
policies: {
maxAdvanceBookingDays: (policies?.maxAdvanceBookingDays as number | undefined) ?? '',
minCancellationNoticeHours:
(policies?.minCancellationNoticeHours as number | undefined) ?? '',
allowOverbooking: (policies?.allowOverbooking as boolean) ?? false,
},
};
}
function buildRulesData(formRules: PlanFormRules, priceOverrides: PriceOverride[]): unknown {
const validOverrides = priceOverrides.filter((o) => o.country.trim());
const { limits, features, policies } = formRules;
const rules: Record<string, unknown> = {
version: validOverrides.length > 0 ? 'v2' : 'v1',
limits: {
maxCourts: limits.maxCourts,
maxBookingsPerDay: limits.maxBookingsPerDay,
...(limits.maxActiveUsers !== '' && { maxActiveUsers: limits.maxActiveUsers }),
...(limits.maxSports !== '' && { maxSports: limits.maxSports }),
},
features,
policies: {
...(policies.maxAdvanceBookingDays !== '' && {
maxAdvanceBookingDays: policies.maxAdvanceBookingDays,
}),
...(policies.minCancellationNoticeHours !== '' && {
minCancellationNoticeHours: policies.minCancellationNoticeHours,
}),
allowOverbooking: policies.allowOverbooking,
},
};
if (validOverrides.length > 0) {
const overrides: Record<string, { amount: number; currency: string }> = {};
for (const o of validOverrides) {
overrides[o.country.trim().toUpperCase()] = {
amount: Number.parseFloat(o.amount),
currency: o.currency,
};
}
rules.pricing = { overrides };
}
return rules;
}
const TABS = [
{ id: 'basico', label: 'Básico' },
{ id: 'limites', label: 'Límites' },
{ id: 'features', label: 'Funcionalidades' },
{ id: 'policies', label: 'Políticas' },
{ id: 'pricing', label: 'Precios x país' },
] as const;
function Toggle({
checked,
onChange,
label,
}: {
checked: boolean;
onChange: (v: boolean) => void;
label: string;
}) {
return (
<button
type="button"
role="switch"
aria-checked={checked}
onClick={() => onChange(!checked)}
className={`relative inline-flex h-6 w-10 shrink-0 items-center rounded-full transition-colors ${
checked ? 'bg-primary' : 'bg-input'
}`}
>
<span
className={`inline-block size-4 rounded-full bg-white shadow-sm transition-transform ${
checked ? 'translate-x-5' : 'translate-x-1'
}`}
/>
<span className="sr-only">{label}</span>
</button>
);
}
function PlanFormDialog({
mode,
plan,
onClose,
}: {
mode: 'create' | 'edit';
plan?: Plan;
onClose: () => void;
}) {
const queryClient = useQueryClient();
const [activeTab, setActiveTab] = useState('basico');
const [code, setCode] = useState(plan?.code ?? '');
const [name, setName] = useState(plan?.name ?? '');
const [price, setPrice] = useState(String(plan?.price ?? ''));
const [formRules, setFormRules] = useState<PlanFormRules>(() => initRules(plan));
const [priceOverrides, setPriceOverrides] = useState<PriceOverride[]>(() =>
mode === 'edit' && plan ? initOverrides(plan.rules) : []
);
const createMutation = useMutation({
mutationFn: (data: { code: string; name: string; price: number; rules: object }) =>
apiClient.admin.createPlan(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['admin-plans'] });
onClose();
},
});
const updateMutation = useMutation({
mutationFn: (data: { name?: string; price?: number; rules?: unknown }) =>
apiClient.admin.updatePlan(plan!.code, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['admin-plans'] });
onClose();
},
});
const isPending = createMutation.isPending || updateMutation.isPending;
const error = createMutation.error ?? updateMutation.error;
function updateRule<S extends keyof PlanFormRules>(
section: S,
field: keyof PlanFormRules[S],
value: PlanFormRules[S][keyof PlanFormRules[S]]
) {
setFormRules((prev) => ({
...prev,
[section]: { ...prev[section], [field]: value },
}));
}
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
const rules = buildRulesData(formRules, priceOverrides);
if (mode === 'create') {
createMutation.mutate({
code,
name,
price: Number.parseFloat(price),
rules: rules as object,
});
} else {
updateMutation.mutate({
name,
price: Number.parseFloat(price),
rules,
});
}
};
const addOverride = () => {
setPriceOverrides((prev) => [...prev, { country: '', amount: '', currency: 'ARS' }]);
};
const removeOverride = (index: number) => {
setPriceOverrides((prev) => prev.filter((_, i) => i !== index));
};
const updateOverride = (index: number, field: keyof PriceOverride, value: string) => {
setPriceOverrides((prev) => prev.map((o, i) => (i === index ? { ...o, [field]: value } : o)));
};
return (
<Dialog open onOpenChange={(open) => !open && onClose()}>
<DialogContent className="rounded-2xl sm:max-w-xl">
<DialogHeader>
<DialogTitle>{mode === 'create' ? 'Crear plan' : `Editar: ${plan!.name}`}</DialogTitle>
<DialogDescription>
{mode === 'create'
? 'Agregá un nuevo plan de suscripción.'
: 'Actualizá los datos del plan.'}
</DialogDescription>
</DialogHeader>
<form onSubmit={handleSubmit} className="space-y-5">
<div className="-mx-6 flex gap-0 border-b border-border/60 px-6">
{TABS.map((tab) => (
<button
key={tab.id}
type="button"
onClick={() => setActiveTab(tab.id)}
className={`-mb-px border-b-2 px-3 pb-2 pt-1 text-sm font-medium transition-colors ${
activeTab === tab.id
? 'border-primary text-primary'
: 'border-transparent text-muted-foreground hover:text-foreground'
}`}
>
{tab.label}
</button>
))}
</div>
{activeTab === 'basico' && (
<div className="space-y-4">
{mode === 'create' && (
<div className="space-y-2">
<Label htmlFor="code">Código</Label>
<Input
id="code"
value={code}
onChange={(e) => setCode(e.target.value)}
placeholder="pro"
maxLength={10}
required
/>
</div>
)}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="name">Nombre</Label>
<Input
id="name"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Pro"
maxLength={30}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="price">
Precio USD
<span className="text-xs text-muted-foreground">(fallback global)</span>
</Label>
<Input
id="price"
type="number"
step="0.01"
min="0"
value={price}
onChange={(e) => setPrice(e.target.value)}
placeholder="29.99"
required
/>
</div>
</div>
</div>
)}
{activeTab === 'limites' && (
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="maxCourts">Máx. canchas</Label>
<Input
id="maxCourts"
type="number"
min="1"
value={formRules.limits.maxCourts}
onChange={(e) => updateRule('limits', 'maxCourts', Number(e.target.value))}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="maxBookingsPerDay">Máx. turnos/día</Label>
<Input
id="maxBookingsPerDay"
type="number"
min="1"
value={formRules.limits.maxBookingsPerDay}
onChange={(e) =>
updateRule('limits', 'maxBookingsPerDay', Number(e.target.value))
}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="maxActiveUsers">
Máx. usuarios activos
<span className="text-xs text-muted-foreground">(opcional)</span>
</Label>
<Input
id="maxActiveUsers"
type="number"
min="1"
value={formRules.limits.maxActiveUsers}
onChange={(e) =>
updateRule(
'limits',
'maxActiveUsers',
e.target.value === '' ? '' : Number(e.target.value)
)
}
placeholder="Ilimitado"
/>
</div>
<div className="space-y-2">
<Label htmlFor="maxSports">
Máx. deportes
<span className="text-xs text-muted-foreground">(opcional)</span>
</Label>
<Input
id="maxSports"
type="number"
min="1"
value={formRules.limits.maxSports}
onChange={(e) =>
updateRule(
'limits',
'maxSports',
e.target.value === '' ? '' : Number(e.target.value)
)
}
placeholder="Ilimitado"
/>
</div>
</div>
</div>
)}
{activeTab === 'features' && (
<div className="space-y-4">
{(
[
['onlinePayments', 'Pagos online'],
['publicBookingPage', 'Página de reservas pública'],
['advancedReports', 'Reportes avanzados'],
['whatsappReminders', 'Recordatorios por WhatsApp'],
['fixedSlots', 'Turnos fijos'],
] as const
).map(([key, label]) => (
<div key={key} className="flex items-center justify-between">
<Label className="cursor-pointer">{label}</Label>
<Toggle
checked={formRules.features[key]}
onChange={(v) => updateRule('features', key, v)}
label={label}
/>
</div>
))}
</div>
)}
{activeTab === 'policies' && (
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="maxAdvanceBookingDays">
Anticipación máx. (días)
<span className="text-xs text-muted-foreground">(opcional)</span>
</Label>
<Input
id="maxAdvanceBookingDays"
type="number"
min="1"
value={formRules.policies.maxAdvanceBookingDays}
onChange={(e) =>
updateRule(
'policies',
'maxAdvanceBookingDays',
e.target.value === '' ? '' : Number(e.target.value)
)
}
placeholder="Sin límite"
/>
</div>
<div className="space-y-2">
<Label htmlFor="minCancellationNoticeHours">
Cancelación mín. (horas)
<span className="text-xs text-muted-foreground">(opcional)</span>
</Label>
<Input
id="minCancellationNoticeHours"
type="number"
min="0"
value={formRules.policies.minCancellationNoticeHours}
onChange={(e) =>
updateRule(
'policies',
'minCancellationNoticeHours',
e.target.value === '' ? '' : Number(e.target.value)
)
}
placeholder="Sin restricción"
/>
</div>
</div>
<div className="flex items-center justify-between">
<Label className="cursor-pointer" htmlFor="allowOverbooking">
Permitir sobreventa
</Label>
<Toggle
checked={formRules.policies.allowOverbooking}
onChange={(v) => updateRule('policies', 'allowOverbooking', v)}
label="Permitir sobreventa"
/>
</div>
</div>
)}
{activeTab === 'pricing' && (
<div className="space-y-3">
<div className="flex items-center justify-between">
<Label>Precios por país</Label>
<Button
type="button"
variant="outline"
size="sm"
className="rounded-xl"
onClick={addOverride}
>
<Plus className="mr-1 size-3" />
Agregar
</Button>
</div>
{priceOverrides.length === 0 && (
<p className="text-sm text-muted-foreground">
Sin precios personalizados. Se usa el precio global USD.
</p>
)}
{priceOverrides.map((override, index) => (
<div key={index} className="flex items-end gap-2">
<div className="flex-1 space-y-1">
<Label className="text-xs">País</Label>
<Input
value={override.country}
onChange={(e) => updateOverride(index, 'country', e.target.value)}
placeholder="AR"
maxLength={2}
className="uppercase"
/>
</div>
<div className="flex-1 space-y-1">
<Label className="text-xs">Monto</Label>
<Input
type="number"
step="0.01"
min="0"
value={override.amount}
onChange={(e) => updateOverride(index, 'amount', e.target.value)}
placeholder="14999"
/>
</div>
<div className="w-24 space-y-1">
<Label className="text-xs">Moneda</Label>
<Select
value={override.currency}
onValueChange={(v) => updateOverride(index, 'currency', v)}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="USD">USD</SelectItem>
<SelectItem value="ARS">ARS</SelectItem>
</SelectContent>
</Select>
</div>
<Button
type="button"
variant="ghost"
size="icon"
className="size-9 shrink-0 rounded-lg text-destructive hover:bg-destructive/10"
onClick={() => removeOverride(index)}
>
<X className="size-3.5" />
</Button>
</div>
))}
</div>
)}
{error && (
<p className="text-sm text-destructive">
{error instanceof ApiClientError
? String(error.details ?? error.message)
: error.message}
</p>
)}
<div className="flex justify-end gap-3">
<Button type="button" variant="outline" className="rounded-xl" onClick={onClose}>
Cancelar
</Button>
<Button type="submit" className="rounded-xl" disabled={isPending}>
{isPending ? 'Guardando...' : mode === 'create' ? 'Crear' : 'Guardar'}
</Button>
</div>
</form>
</DialogContent>
</Dialog>
);
}
function DeleteConfirmDialog({
plan,
onClose,
}: {
plan: Plan;
onClose: () => void;
}) {
const queryClient = useQueryClient();
const deleteMutation = useMutation({
mutationFn: () => apiClient.admin.deletePlan(plan.code),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['admin-plans'] });
onClose();
},
});
return (
<Dialog open onOpenChange={(open) => !open && onClose()}>
<DialogContent className="rounded-2xl sm:max-w-md">
<DialogHeader>
<DialogTitle>Eliminar plan</DialogTitle>
<DialogDescription>
¿Estás seguro de eliminar el plan <strong>{plan.name}</strong>?
{plan.complexCount > 0 && (
<span className="mt-2 block text-destructive">
No se puede eliminar porque tiene {plan.complexCount} complejo
{plan.complexCount > 1 ? 's' : ''} asignado{plan.complexCount > 1 ? 's' : ''}.
</span>
)}
</DialogDescription>
</DialogHeader>
{deleteMutation.error && (
<p className="text-sm text-destructive">
{deleteMutation.error instanceof ApiClientError
? String(deleteMutation.error.details ?? deleteMutation.error.message)
: deleteMutation.error.message}
</p>
)}
<div className="flex justify-end gap-3">
<Button type="button" variant="outline" className="rounded-xl" onClick={onClose}>
Cancelar
</Button>
<Button
type="button"
variant="destructive"
className="rounded-xl"
onClick={() => deleteMutation.mutate()}
disabled={deleteMutation.isPending || plan.complexCount > 0}
>
{deleteMutation.isPending ? 'Eliminando...' : 'Eliminar'}
</Button>
</div>
</DialogContent>
</Dialog>
);
}
export function PlansPage() {
const location = useLocation();
const queryClient = useQueryClient();
const [createOpen, setCreateOpen] = useState(false);
const [editingPlan, setEditingPlan] = useState<Plan | null>(null);
const [deletingPlan, setDeletingPlan] = useState<Plan | null>(null);
const { data, isLoading } = useQuery({
queryKey: ['admin-plans'],
queryFn: () => apiClient.admin.listPlans(),
});
return (
<AdminLayout currentPath={location.pathname}>
<div className="mb-6 flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold tracking-tight">Planes</h1>
<p className="mt-1 text-sm text-muted-foreground">
{data?.length ?? 0} planes de suscripción.
</p>
</div>
<div className="flex gap-2">
<Button
variant="outline"
size="icon"
className="rounded-xl"
onClick={() => queryClient.invalidateQueries({ queryKey: ['admin-plans'] })}
>
<RefreshCw className="size-4" />
</Button>
<Button className="rounded-xl" onClick={() => setCreateOpen(true)}>
<Plus className="mr-2 size-4" />
Nuevo plan
</Button>
</div>
</div>
{createOpen && <PlanFormDialog mode="create" onClose={() => setCreateOpen(false)} />}
{editingPlan && (
<PlanFormDialog mode="edit" plan={editingPlan} onClose={() => setEditingPlan(null)} />
)}
{deletingPlan && (
<DeleteConfirmDialog plan={deletingPlan} onClose={() => setDeletingPlan(null)} />
)}
{isLoading ? (
<div className="flex items-center justify-center py-20">
<p className="text-sm text-muted-foreground">Cargando planes...</p>
</div>
) : data?.length === 0 ? (
<div className="flex flex-col items-center justify-center py-20">
<BarChart3 className="mb-3 size-10 text-muted-foreground/50" />
<p className="text-sm text-muted-foreground">No hay planes creados.</p>
</div>
) : (
<div className="overflow-hidden rounded-2xl border border-border/60">
<table className="w-full">
<thead>
<tr className="border-b border-border/60 bg-muted/50">
<th className="px-5 py-3 text-left text-xs font-semibold uppercase tracking-wider text-muted-foreground">
Código
</th>
<th className="px-5 py-3 text-left text-xs font-semibold uppercase tracking-wider text-muted-foreground">
Nombre
</th>
<th className="px-5 py-3 text-right text-xs font-semibold uppercase tracking-wider text-muted-foreground">
Precio
</th>
<th className="hidden px-5 py-3 text-center text-xs font-semibold uppercase tracking-wider text-muted-foreground sm:table-cell">
Complejos
</th>
<th className="px-5 py-3 text-right text-xs font-semibold uppercase tracking-wider text-muted-foreground">
Acciones
</th>
</tr>
</thead>
<tbody className="divide-y divide-border/40">
{data?.map((plan) => {
const rules = plan.rules as Record<string, unknown> | null;
const pricing = rules?.pricing as Record<string, unknown> | undefined;
const overrides = pricing?.overrides as Record<string, unknown> | undefined;
const overrideCountries = overrides ? Object.keys(overrides) : [];
return (
<tr key={plan.code} className="transition-colors hover:bg-accent/30">
<td className="px-5 py-4">
<Badge variant="secondary" className="font-mono text-xs">
{plan.code}
</Badge>
</td>
<td className="px-5 py-4 font-medium">{plan.name}</td>
<td className="px-5 py-4 text-right">
<div className="flex flex-col items-end gap-1">
<span className="font-medium">${Number(plan.price).toFixed(2)} USD</span>
{overrideCountries.length > 0 && (
<div className="flex flex-wrap gap-1">
{overrideCountries.map((country) => (
<Badge key={country} variant="secondary" className="text-xs">
{country}
</Badge>
))}
</div>
)}
</div>
</td>
<td className="hidden px-5 py-4 text-center text-sm text-muted-foreground sm:table-cell">
{plan.complexCount}
</td>
<td className="px-5 py-4 text-right">
<div className="flex justify-end gap-1">
<Button
variant="ghost"
size="icon"
className="size-8 rounded-lg"
onClick={() => setEditingPlan(plan)}
>
<Pencil className="size-3.5" />
</Button>
<Button
variant="ghost"
size="icon"
className="size-8 rounded-lg text-destructive hover:bg-destructive/10 hover:text-destructive"
onClick={() => setDeletingPlan(plan)}
>
<Trash2 className="size-3.5" />
</Button>
</div>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
</AdminLayout>
);
}

View File

@@ -0,0 +1,467 @@
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { AdminLayout } from '@/features/admin/admin-layout';
import { ApiClientError, apiClient } from '@/lib/api-client';
import type { AdminUser, AdminUserSession } from '@repo/api-contract';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useLocation } from '@tanstack/react-router';
import {
Ban,
CheckCircle2,
Globe,
LogIn,
MapPin,
Monitor,
RefreshCw,
Search,
ShieldAlert,
Users,
} from 'lucide-react';
import { useEffect, useState } from 'react';
function SessionsDialog({
user,
onClose,
}: {
user: AdminUser;
onClose: () => void;
}) {
const queryClient = useQueryClient();
const { data, isLoading } = useQuery({
queryKey: ['admin-user-sessions', user.id],
queryFn: () => apiClient.admin.getUserSessions(user.id),
});
const revokeMutation = useMutation({
mutationFn: () => apiClient.admin.revokeAllSessions(user.id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['admin-user-sessions', user.id] });
queryClient.invalidateQueries({ queryKey: ['admin-users'] });
},
});
return (
<Dialog open onOpenChange={(open) => !open && onClose()}>
<DialogContent className="rounded-2xl sm:max-w-lg">
<DialogHeader>
<DialogTitle>Sesiones de {user.name}</DialogTitle>
<DialogDescription>{data?.length ?? 0} sesiones activas</DialogDescription>
</DialogHeader>
<div className="max-h-80 space-y-3 overflow-y-auto">
{isLoading ? (
<p className="py-8 text-center text-sm text-muted-foreground">Cargando sesiones...</p>
) : data?.length === 0 ? (
<p className="py-8 text-center text-sm text-muted-foreground">Sin sesiones activas.</p>
) : (
data?.map((session) => <SessionRow key={session.id} session={session} />)
)}
</div>
<div className="flex items-center justify-between gap-3">
{revokeMutation.error && (
<p className="flex-1 text-sm text-destructive">
{revokeMutation.error instanceof ApiClientError
? String(revokeMutation.error.details ?? revokeMutation.error.message)
: revokeMutation.error.message}
</p>
)}
<div className="flex gap-3">
<Button variant="outline" className="rounded-xl" onClick={onClose}>
Cerrar
</Button>
{data && data.length > 0 && (
<Button
variant="destructive"
className="rounded-xl"
onClick={() => revokeMutation.mutate()}
disabled={revokeMutation.isPending}
>
{revokeMutation.isPending ? 'Cerrando...' : 'Cerrar todas las sesiones'}
</Button>
)}
</div>
</div>
</DialogContent>
</Dialog>
);
}
function SessionRow({ session }: { session: AdminUserSession }) {
const isExpired = new Date(session.expiresAt) < new Date();
const userAgent = session.userAgent ?? 'Desconocido';
const ip = session.ipAddress ?? '—';
const location =
session.city || session.country
? [session.city, session.country].filter(Boolean).join(', ')
: null;
return (
<div className="rounded-xl border border-border/60 p-4">
<div className="mb-2 flex items-center gap-2">
<Monitor className="size-4 text-muted-foreground" />
<span className="flex-1 truncate text-sm font-medium">{userAgent}</span>
{isExpired ? (
<Badge
variant="outline"
className="border-gray-300 bg-gray-50 text-gray-600 dark:border-gray-700 dark:bg-gray-950 dark:text-gray-400"
>
Expirada
</Badge>
) : (
<Badge
variant="outline"
className="border-emerald-300 bg-emerald-50 text-emerald-700 dark:border-emerald-700 dark:bg-emerald-950 dark:text-emerald-400"
>
Activa
</Badge>
)}
</div>
<div className="flex flex-wrap gap-x-4 gap-y-1 text-xs text-muted-foreground">
<span className="flex items-center gap-1">
<Globe className="size-3" />
{ip}
</span>
{location && (
<span className="flex items-center gap-1">
<MapPin className="size-3" />
{location}
</span>
)}
<span className="flex items-center gap-1">
<LogIn className="size-3" />
{new Date(session.createdAt).toLocaleString()}
</span>
</div>
</div>
);
}
function BlockDialog({
user,
onClose,
}: {
user: AdminUser;
onClose: () => void;
}) {
const queryClient = useQueryClient();
const [reason, setReason] = useState('');
const mutation = useMutation({
mutationFn: (banReason?: string) => apiClient.admin.blockUser(user.id, { banReason }),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['admin-users'] });
onClose();
},
});
return (
<Dialog open onOpenChange={(open) => !open && onClose()}>
<DialogContent className="rounded-2xl sm:max-w-md">
<DialogHeader>
<DialogTitle>Bloquear usuario</DialogTitle>
<DialogDescription>
¿Estás seguro de bloquear a <strong>{user.name}</strong> ({user.email})? No podrá
iniciar sesión en Playzer.
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="reason">Motivo (opcional)</Label>
<Input
id="reason"
value={reason}
onChange={(e) => setReason(e.target.value)}
placeholder="Violación de términos..."
maxLength={500}
/>
</div>
{mutation.error && (
<p className="text-sm text-destructive">
{mutation.error instanceof ApiClientError
? String(mutation.error.details ?? mutation.error.message)
: mutation.error.message}
</p>
)}
<div className="flex justify-end gap-3">
<Button type="button" variant="outline" className="rounded-xl" onClick={onClose}>
Cancelar
</Button>
<Button
type="button"
variant="destructive"
className="rounded-xl"
onClick={() => mutation.mutate(reason || undefined)}
disabled={mutation.isPending}
>
{mutation.isPending ? 'Bloqueando...' : 'Bloquear'}
</Button>
</div>
</div>
</DialogContent>
</Dialog>
);
}
function UnblockDialog({
user,
onClose,
}: {
user: AdminUser;
onClose: () => void;
}) {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: () => apiClient.admin.unblockUser(user.id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['admin-users'] });
onClose();
},
});
return (
<Dialog open onOpenChange={(open) => !open && onClose()}>
<DialogContent className="rounded-2xl sm:max-w-md">
<DialogHeader>
<DialogTitle>Desbloquear usuario</DialogTitle>
<DialogDescription>
¿Estás seguro de desbloquear a <strong>{user.name}</strong>? Podrá volver a iniciar
sesión en Playzer.
</DialogDescription>
</DialogHeader>
{mutation.error && (
<p className="text-sm text-destructive">
{mutation.error instanceof ApiClientError
? String(mutation.error.details ?? mutation.error.message)
: mutation.error.message}
</p>
)}
<div className="flex justify-end gap-3">
<Button type="button" variant="outline" className="rounded-xl" onClick={onClose}>
Cancelar
</Button>
<Button
type="button"
variant="default"
className="rounded-xl"
onClick={() => mutation.mutate()}
disabled={mutation.isPending}
>
{mutation.isPending ? 'Desbloqueando...' : 'Desbloquear'}
</Button>
</div>
</DialogContent>
</Dialog>
);
}
function RoleBadge({ role }: { role: string }) {
if (role === 'super_admin') {
return (
<Badge
variant="outline"
className="border-purple-300 bg-purple-50 text-purple-700 dark:border-purple-700 dark:bg-purple-950 dark:text-purple-400"
>
<ShieldAlert className="mr-1 size-3" />
Super Admin
</Badge>
);
}
return (
<Badge variant="secondary" className="text-xs">
{role}
</Badge>
);
}
export function UsersPage() {
const location = useLocation();
const queryClient = useQueryClient();
const [blockingUser, setBlockingUser] = useState<AdminUser | null>(null);
const [unblockingUser, setUnblockingUser] = useState<AdminUser | null>(null);
const [sessionsUser, setSessionsUser] = useState<AdminUser | null>(null);
const [searchInput, setSearchInput] = useState('');
const [search, setSearch] = useState('');
useEffect(() => {
const timer = setTimeout(() => setSearch(searchInput), 300);
return () => clearTimeout(timer);
}, [searchInput]);
const { data, isLoading } = useQuery({
queryKey: ['admin-users', search],
queryFn: () => apiClient.admin.listUsers(search || undefined),
});
return (
<AdminLayout currentPath={location.pathname}>
<div className="mb-6 flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold tracking-tight">Usuarios</h1>
<p className="mt-1 text-sm text-muted-foreground">
{data?.length ?? 0} usuarios registrados en Playzer.
</p>
</div>
<div className="flex items-center gap-2">
<div className="relative">
<Search className="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
<Input
value={searchInput}
onChange={(e) => setSearchInput(e.target.value)}
placeholder="Buscar por nombre o email..."
className="h-9 w-56 rounded-xl pl-9 text-sm"
/>
</div>
<Button
variant="outline"
size="icon"
className="rounded-xl"
onClick={() => queryClient.invalidateQueries({ queryKey: ['admin-users'] })}
>
<RefreshCw className="size-4" />
</Button>
</div>
</div>
{blockingUser && <BlockDialog user={blockingUser} onClose={() => setBlockingUser(null)} />}
{unblockingUser && (
<UnblockDialog user={unblockingUser} onClose={() => setUnblockingUser(null)} />
)}
{sessionsUser && <SessionsDialog user={sessionsUser} onClose={() => setSessionsUser(null)} />}
{isLoading ? (
<div className="flex items-center justify-center py-20">
<p className="text-sm text-muted-foreground">Cargando usuarios...</p>
</div>
) : data?.length === 0 ? (
<div className="flex flex-col items-center justify-center py-20">
<Users className="mb-3 size-10 text-muted-foreground/50" />
<p className="text-sm text-muted-foreground">No hay usuarios registrados.</p>
</div>
) : (
<div className="overflow-hidden rounded-2xl border border-border/60">
<table className="w-full">
<thead>
<tr className="border-b border-border/60 bg-muted/50">
<th className="px-5 py-3 text-left text-xs font-semibold uppercase tracking-wider text-muted-foreground">
Usuario
</th>
<th className="hidden px-5 py-3 text-left text-xs font-semibold uppercase tracking-wider text-muted-foreground md:table-cell">
Rol
</th>
<th className="hidden px-5 py-3 text-center text-xs font-semibold uppercase tracking-wider text-muted-foreground sm:table-cell">
Complejos
</th>
<th className="hidden px-5 py-3 text-center text-xs font-semibold uppercase tracking-wider text-muted-foreground lg:table-cell">
Sesiones
</th>
<th className="hidden px-5 py-3 text-center text-xs font-semibold uppercase tracking-wider text-muted-foreground lg:table-cell">
Registro
</th>
<th className="px-5 py-3 text-center text-xs font-semibold uppercase tracking-wider text-muted-foreground">
Estado
</th>
<th className="px-5 py-3 text-right text-xs font-semibold uppercase tracking-wider text-muted-foreground">
Acción
</th>
</tr>
</thead>
<tbody className="divide-y divide-border/40">
{data?.map((user) => (
<tr key={user.id} className="transition-colors hover:bg-accent/30">
<td className="px-5 py-4">
<div className="min-w-0">
<p className="truncate font-medium">{user.name}</p>
<p className="truncate text-xs text-muted-foreground">{user.email}</p>
</div>
</td>
<td className="hidden px-5 py-4 md:table-cell">
<RoleBadge role={user.role} />
</td>
<td className="hidden px-5 py-4 text-center text-sm text-muted-foreground sm:table-cell">
{user.complexCount}
</td>
<td className="hidden px-5 py-4 text-center lg:table-cell">
<button
type="button"
className="cursor-pointer text-sm font-medium text-emerald-600 underline-offset-2 hover:underline"
onClick={() => setSessionsUser(user)}
>
{user.activeSessions}
</button>
</td>
<td className="hidden px-5 py-4 text-sm text-muted-foreground lg:table-cell">
{new Date(user.createdAt).toLocaleDateString()}
</td>
<td className="px-5 py-4 text-center">
{user.banned ? (
<Badge
variant="outline"
className="border-red-300 bg-red-50 text-red-700 dark:border-red-700 dark:bg-red-950 dark:text-red-400"
>
Bloqueado
</Badge>
) : (
<Badge
variant="outline"
className="border-emerald-300 bg-emerald-50 text-emerald-700 dark:border-emerald-700 dark:bg-emerald-950 dark:text-emerald-400"
>
Activo
</Badge>
)}
</td>
<td className="px-5 py-4 text-right">
<div className="flex items-center justify-end gap-1">
{user.banned ? (
<Button
variant="ghost"
size="sm"
className="rounded-xl text-xs"
disabled={user.role === 'super_admin'}
onClick={() => setUnblockingUser(user)}
>
<CheckCircle2 className="mr-1.5 size-3.5 text-emerald-600" />
Desbloquear
</Button>
) : (
<Button
variant="ghost"
size="sm"
className="rounded-xl text-xs text-destructive hover:bg-destructive/10 hover:text-destructive disabled:opacity-40"
disabled={user.role === 'super_admin'}
onClick={() => setBlockingUser(user)}
>
<Ban className="mr-1.5 size-3.5" />
Bloquear
</Button>
)}
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</AdminLayout>
);
}

View File

@@ -1,5 +1,11 @@
import { ApiClientError, apiClient } from '@/lib/api-client'; import { ApiClientError, apiClient } from '@/lib/api-client';
import type { AdminBooking, ComplexWithRole, Court } from '@repo/api-contract'; import type {
AdminBooking,
ComplexWithRole,
Court,
PlanFeatureFlags,
RecurringBookingGroup,
} from '@repo/api-contract';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { import {
type ReactNode, type ReactNode,
@@ -44,6 +50,16 @@ interface CreateManualBookingPayload {
customerEmail: string; customerEmail: string;
} }
interface CreateRecurringBookingPayload {
courtId: string;
date: string;
startTime: string;
customerName: string;
customerPhone: string;
customerEmail: string;
recurringEndDate?: string;
}
interface BookingContextValue { interface BookingContextValue {
complex: ComplexWithRole; complex: ComplexWithRole;
courts: Court[]; courts: Court[];
@@ -66,6 +82,8 @@ interface BookingContextValue {
createBookingError: string | null; createBookingError: string | null;
isCreatingBooking: boolean; isCreatingBooking: boolean;
bookingToolsOpen: boolean; bookingToolsOpen: boolean;
planFeatures: PlanFeatureFlags | null;
isRecurringEnabled: boolean;
setSelectedDate: (date: string) => void; setSelectedDate: (date: string) => void;
moveSelectedDate: (amount: number) => void; moveSelectedDate: (amount: number) => void;
setSelectedSportId: (sportId: string) => void; setSelectedSportId: (sportId: string) => void;
@@ -73,8 +91,14 @@ interface BookingContextValue {
setViewMode: (mode: BookingViewMode) => void; setViewMode: (mode: BookingViewMode) => void;
setVisibleTimeRange: (range: BookingTimeRange) => void; setVisibleTimeRange: (range: BookingTimeRange) => void;
openCreateBooking: (draft?: Partial<BookingSlotDraft>) => void; openCreateBooking: (draft?: Partial<BookingSlotDraft>) => void;
recurringGroups: RecurringBookingGroup[];
isRecurringGroupsLoading: boolean;
cancelRecurringGroup: (groupId: string) => void;
isCancellingRecurringGroup: boolean;
refreshRecurringGroups: () => void;
closeCreateBooking: () => void; closeCreateBooking: () => void;
createBooking: (payload: CreateManualBookingPayload) => Promise<void>; createBooking: (payload: CreateManualBookingPayload) => Promise<void>;
createRecurringBooking: (payload: CreateRecurringBookingPayload) => Promise<void>;
updateBookingStatus: (bookingId: string, status: 'CANCELLED' | 'COMPLETED' | 'NOSHOW') => void; updateBookingStatus: (bookingId: string, status: 'CANCELLED' | 'COMPLETED' | 'NOSHOW') => void;
exportDayReport: () => void; exportDayReport: () => void;
openBookingTools: (segment?: BookingTimelineSegment) => void; openBookingTools: (segment?: BookingTimelineSegment) => void;
@@ -147,6 +171,9 @@ function buildSegmentsForCourt(
const segments: BookingTimelineSegment[] = []; const segments: BookingTimelineSegment[] = [];
const slotDuration = court.slotDurationMinutes; const slotDuration = court.slotDurationMinutes;
const nowMinutes = timeToMinutes(getNowTime());
const isToday = isTodayIso(selectedDate);
const availabilityRanges = court.availability const availabilityRanges = court.availability
.filter((range) => range.dayOfWeek === dayOfWeek) .filter((range) => range.dayOfWeek === dayOfWeek)
.map((range) => ({ .map((range) => ({
@@ -181,6 +208,9 @@ function buildSegmentsForCourt(
slotStart += slotDuration slotStart += slotDuration
) { ) {
const slotEnd = slotStart + slotDuration; const slotEnd = slotStart + slotDuration;
if (isToday && slotEnd <= nowMinutes) continue;
const isBooked = courtBookings.some((booking) => const isBooked = courtBookings.some((booking) =>
overlapsBooking(slotStart, slotEnd, booking) overlapsBooking(slotStart, slotEnd, booking)
); );
@@ -239,6 +269,20 @@ export function BookingProvider({ children, complex }: BookingProviderProps) {
queryFn: () => apiClient.courts.listByComplex(complex.id), queryFn: () => apiClient.courts.listByComplex(complex.id),
}); });
const recurringGroupsQuery = useQuery({
queryKey: ['recurring-groups', complex.id],
queryFn: () => apiClient.adminBookings.listRecurringGroups(complex.id),
enabled: viewMode === 'recurring',
});
const cancelRecurringGroupMutation = useMutation({
mutationFn: (groupId: string) => apiClient.adminBookings.cancelRecurringGroup(groupId),
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: ['recurring-groups', complex.id] });
await queryClient.invalidateQueries({ queryKey: ['admin-bookings', complex.id] });
},
});
const bookingsQuery = useQuery({ const bookingsQuery = useQuery({
queryKey: ['admin-bookings', complex.id, selectedDate], queryKey: ['admin-bookings', complex.id, selectedDate],
queryFn: () => queryFn: () =>
@@ -319,6 +363,26 @@ export function BookingProvider({ children, complex }: BookingProviderProps) {
}, },
}); });
const createRecurringBookingMutation = useMutation({
mutationFn: (payload: CreateRecurringBookingPayload) =>
apiClient.adminBookings.createRecurring(complex.id, {
date: payload.date,
courtId: payload.courtId,
startTime: payload.startTime,
customerName: payload.customerName,
customerPhone: payload.customerPhone,
customerEmail: payload.customerEmail,
isRecurring: true,
recurringEndDate: payload.recurringEndDate,
}),
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: ['admin-bookings', complex.id] });
await queryClient.invalidateQueries({ queryKey: ['manual-booking-availability'] });
setIsCreateBookingOpen(false);
setSelectedSlot(null);
},
});
const updateStatusMutation = useMutation({ const updateStatusMutation = useMutation({
mutationFn: (payload: { bookingId: string; status: 'CANCELLED' | 'COMPLETED' | 'NOSHOW' }) => mutationFn: (payload: { bookingId: string; status: 'CANCELLED' | 'COMPLETED' | 'NOSHOW' }) =>
apiClient.adminBookings.updateStatus(payload.bookingId, { status: payload.status }), apiClient.adminBookings.updateStatus(payload.bookingId, { status: payload.status }),
@@ -407,6 +471,16 @@ export function BookingProvider({ children, complex }: BookingProviderProps) {
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
}, [bookings, complex.complexSlug, selectedDate]); }, [bookings, complex.complexSlug, selectedDate]);
const planFeatures = complex.planFeatures ?? null;
const isRecurringEnabled = planFeatures?.fixedSlots ?? false;
const createRecurringBooking = useCallback(
async (payload: CreateRecurringBookingPayload) => {
await createRecurringBookingMutation.mutateAsync(payload);
},
[createRecurringBookingMutation]
);
const value = useMemo<BookingContextValue>( const value = useMemo<BookingContextValue>(
() => ({ () => ({
complex, complex,
@@ -431,11 +505,24 @@ export function BookingProvider({ children, complex }: BookingProviderProps) {
'No pudimos cargar el panel de reservas.' 'No pudimos cargar el panel de reservas.'
) )
: null, : null,
recurringGroups: recurringGroupsQuery.data?.groups ?? [],
isRecurringGroupsLoading: recurringGroupsQuery.isLoading,
cancelRecurringGroup: (groupId: string) => cancelRecurringGroupMutation.mutate(groupId),
isCancellingRecurringGroup: cancelRecurringGroupMutation.isPending,
refreshRecurringGroups: () =>
queryClient.invalidateQueries({ queryKey: ['recurring-groups', complex.id] }),
isCreateBookingOpen, isCreateBookingOpen,
createBookingError: createBookingMutation.isError createBookingError:
? extractMessage(createBookingMutation.error, 'No pudimos crear la reserva.') createBookingMutation.isError || createRecurringBookingMutation.isError
? extractMessage(
createBookingMutation.error ?? createRecurringBookingMutation.error,
'No pudimos crear la reserva.'
)
: null, : null,
isCreatingBooking: createBookingMutation.isPending, isCreatingBooking:
createBookingMutation.isPending || createRecurringBookingMutation.isPending,
planFeatures,
isRecurringEnabled,
setSelectedDate, setSelectedDate,
moveSelectedDate: (amount) => setSelectedDate((date) => addDaysIso(date, amount)), moveSelectedDate: (amount) => setSelectedDate((date) => addDaysIso(date, amount)),
setSelectedSportId, setSelectedSportId,
@@ -445,6 +532,7 @@ export function BookingProvider({ children, complex }: BookingProviderProps) {
openCreateBooking, openCreateBooking,
closeCreateBooking, closeCreateBooking,
createBooking, createBooking,
createRecurringBooking,
updateBookingStatus, updateBookingStatus,
exportDayReport, exportDayReport,
bookingToolsOpen, bookingToolsOpen,
@@ -467,10 +555,16 @@ export function BookingProvider({ children, complex }: BookingProviderProps) {
createBookingMutation.error, createBookingMutation.error,
createBookingMutation.isError, createBookingMutation.isError,
createBookingMutation.isPending, createBookingMutation.isPending,
createRecurringBooking,
createRecurringBookingMutation.error,
createRecurringBookingMutation.isError,
createRecurringBookingMutation.isPending,
currentTime, currentTime,
exportDayReport, exportDayReport,
isCreateBookingOpen, isCreateBookingOpen,
openCreateBooking, openCreateBooking,
planFeatures,
isRecurringEnabled,
schedules, schedules,
selectedDate, selectedDate,
selectedSlot, selectedSlot,
@@ -482,6 +576,9 @@ export function BookingProvider({ children, complex }: BookingProviderProps) {
viewMode, viewMode,
visibleTimeRange, visibleTimeRange,
bookingToolsOpen, bookingToolsOpen,
recurringGroupsQuery.data?.groups,
recurringGroupsQuery.isLoading,
cancelRecurringGroupMutation.isPending,
] ]
); );

View File

@@ -3,21 +3,22 @@ import { authClient } from '@/lib/api-client';
import { useAuth } from '@/lib/auth'; import { useAuth } from '@/lib/auth';
import type { ComplexWithRole } from '@repo/api-contract'; import type { ComplexWithRole } from '@repo/api-contract';
import { useState } from 'react'; import { useState } from 'react';
import { BookingProvider } from './booking-provider'; import { BookingProvider, useBooking } from './booking-provider';
import { BookingCreateDialog } from './components/booking-create-dialog'; import { BookingCreateDialog } from './components/booking-create-dialog';
import { BookingDaySummary, BookingQuickActions } from './components/booking-day-summary'; import { BookingDaySummary, BookingQuickActions } from './components/booking-day-summary';
import { BookingHeader } from './components/booking-header'; import { BookingHeader } from './components/booking-header';
import { BookingListView } from './components/booking-list-view';
import { BookingMobile } from './components/booking-mobile'; import { BookingMobile } from './components/booking-mobile';
import { BookingTimeline } from './components/booking-timeline'; import { BookingTimeline } from './components/booking-timeline';
import { BookingToolbar } from './components/booking-toolbar'; import { BookingToolbar } from './components/booking-toolbar';
import { BookingToolsDialog } from './components/booking-tools-dialog'; import { BookingToolsDialog } from './components/booking-tools-dialog';
import { RecurringBookingListView } from './components/recurring-booking-list';
interface BookingProps { interface BookingProps {
complex: ComplexWithRole; complex: ComplexWithRole;
} }
export function Booking({ complex }: BookingProps) { export function Booking({ complex }: BookingProps) {
const isMobile = useIsMobile();
const { user } = useAuth(); const { user } = useAuth();
const [sending, setSending] = useState(false); const [sending, setSending] = useState(false);
const [emailSent, setEmailSent] = useState(false); const [emailSent, setEmailSent] = useState(false);
@@ -56,9 +57,30 @@ export function Booking({ complex }: BookingProps) {
</button> </button>
</div> </div>
)} )}
{isMobile ? ( <BookingInner />
<BookingMobile /> <BookingCreateDialog />
) : ( <BookingToolsDialog />
</BookingProvider>
);
}
function BookingInner() {
const { viewMode } = useBooking();
const isMobile = useIsMobile();
if (viewMode === 'list') {
return <BookingListView />;
}
if (viewMode === 'recurring') {
return <RecurringBookingListView />;
}
if (isMobile) {
return <BookingMobile />;
}
return (
<div className="flex flex-col gap-5"> <div className="flex flex-col gap-5">
<BookingHeader /> <BookingHeader />
<BookingToolbar /> <BookingToolbar />
@@ -68,9 +90,5 @@ export function Booking({ complex }: BookingProps) {
<BookingQuickActions /> <BookingQuickActions />
</div> </div>
</div> </div>
)}
<BookingCreateDialog />
<BookingToolsDialog />
</BookingProvider>
); );
} }

View File

@@ -1,6 +1,6 @@
import type { AdminBooking, Court } from '@repo/api-contract'; import type { AdminBooking, Court } from '@repo/api-contract';
export type BookingViewMode = 'panel' | 'status'; export type BookingViewMode = 'panel' | 'status' | 'list' | 'recurring';
export type BookingStatusFilter = 'all' | 'free' | 'reserved'; export type BookingStatusFilter = 'all' | 'free' | 'reserved';
export type BookingSegmentStatus = 'free' | 'reserved'; export type BookingSegmentStatus = 'free' | 'reserved';

View File

@@ -18,6 +18,7 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from '@/components/ui/select'; } from '@/components/ui/select';
import { Separator } from '@/components/ui/separator';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
@@ -26,6 +27,8 @@ import { useBooking } from '../booking-provider';
import { import {
fromIsoDateLocal, fromIsoDateLocal,
getDayOfWeek, getDayOfWeek,
getNowTime,
isTodayIso,
minutesToTime, minutesToTime,
timeToMinutes, timeToMinutes,
toIsoDateLocal, toIsoDateLocal,
@@ -58,12 +61,16 @@ export function BookingCreateDialog() {
isCreatingBooking, isCreatingBooking,
closeCreateBooking, closeCreateBooking,
createBooking, createBooking,
createRecurringBooking,
isRecurringEnabled,
} = useBooking(); } = useBooking();
const [date, setDate] = useState(selectedSlot?.date ?? selectedDate); const [date, setDate] = useState(selectedSlot?.date ?? selectedDate);
const [sportId, setSportId] = useState(selectedSlot?.sportId ?? 'all'); const [sportId, setSportId] = useState(selectedSlot?.sportId ?? 'all');
const [courtId, setCourtId] = useState(selectedSlot?.courtId ?? ''); const [courtId, setCourtId] = useState(selectedSlot?.courtId ?? '');
const [startTime, setStartTime] = useState(selectedSlot?.startTime ?? ''); const [startTime, setStartTime] = useState(selectedSlot?.startTime ?? '');
const [isRecurring, setIsRecurring] = useState(false);
const [recurringEndDate, setRecurringEndDate] = useState<string | undefined>(undefined);
const { const {
register, register,
@@ -88,6 +95,8 @@ export function BookingCreateDialog() {
setSportId(selectedSlot?.sportId ?? 'all'); setSportId(selectedSlot?.sportId ?? 'all');
setCourtId(selectedSlot?.courtId ?? ''); setCourtId(selectedSlot?.courtId ?? '');
setStartTime(selectedSlot?.startTime ?? ''); setStartTime(selectedSlot?.startTime ?? '');
setIsRecurring(false);
setRecurringEndDate(undefined);
reset(); reset();
if (selectedSlot?.courtId && selectedSlot?.startTime) { if (selectedSlot?.courtId && selectedSlot?.startTime) {
@@ -128,6 +137,9 @@ export function BookingCreateDialog() {
minute += selectedCourt.slotDurationMinutes minute += selectedCourt.slotDurationMinutes
) { ) {
const slotEnd = minute + selectedCourt.slotDurationMinutes; const slotEnd = minute + selectedCourt.slotDurationMinutes;
if (isTodayIso(date) && minute <= timeToMinutes(getNowTime())) continue;
const isBooked = courtBookings.some((booking) => { const isBooked = courtBookings.some((booking) => {
const bookingStart = timeToMinutes(booking.startTime); const bookingStart = timeToMinutes(booking.startTime);
const bookingEnd = timeToMinutes(booking.endTime); const bookingEnd = timeToMinutes(booking.endTime);
@@ -150,6 +162,17 @@ export function BookingCreateDialog() {
}, [availableStartTimes, startTime]); }, [availableStartTimes, startTime]);
const onSubmit = async (values: BookingForm) => { const onSubmit = async (values: BookingForm) => {
if (isRecurring) {
await createRecurringBooking({
courtId,
date,
startTime,
customerName: values.customerName,
customerPhone: values.customerPhone,
customerEmail: values.customerEmail,
recurringEndDate,
});
} else {
await createBooking({ await createBooking({
courtId, courtId,
date, date,
@@ -158,6 +181,7 @@ export function BookingCreateDialog() {
customerPhone: values.customerPhone, customerPhone: values.customerPhone,
customerEmail: values.customerEmail, customerEmail: values.customerEmail,
}); });
}
}; };
return ( return (
@@ -255,6 +279,52 @@ export function BookingCreateDialog() {
</Field> </Field>
</div> </div>
{isRecurringEnabled && (
<>
<Separator />
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<p className="text-sm font-medium">Repetir todas las semanas</p>
<p className="text-xs text-muted-foreground">
{isRecurring
? `Se creará una reserva todos los ${getDayOfWeekLabel(date)} a las ${startTime || '...'}`
: 'Crea turnos fijos en el mismo horario'}
</p>
</div>
<Button
type="button"
variant={isRecurring ? 'default' : 'outline'}
size="sm"
onClick={() => setIsRecurring(!isRecurring)}
className="shrink-0"
>
{isRecurring ? 'Activado' : 'Desactivado'}
</Button>
</div>
{isRecurring && (
<Field>
<FieldLabel>
Repetir hasta{' '}
<span className="text-muted-foreground font-normal">(opcional)</span>
</FieldLabel>
<DatePicker
value={fromIsoDateLocal(recurringEndDate ?? '')}
onChange={(nextDate) => {
setRecurringEndDate(nextDate ? toIsoDateLocal(nextDate) : undefined);
}}
/>
<p className="mt-1 text-xs text-muted-foreground">
Si no se selecciona una fecha, se repetirá por tiempo indefinido.
</p>
</Field>
)}
</>
)}
<Separator />
<Field data-invalid={Boolean(errors.customerName)}> <Field data-invalid={Boolean(errors.customerName)}>
<FieldLabel htmlFor="customerName">Nombre</FieldLabel> <FieldLabel htmlFor="customerName">Nombre</FieldLabel>
<Input <Input
@@ -307,10 +377,29 @@ export function BookingCreateDialog() {
form="booking-create-form" form="booking-create-form"
disabled={!isValid || isCreatingBooking || !courtId || !startTime} disabled={!isValid || isCreatingBooking || !courtId || !startTime}
> >
{isCreatingBooking ? 'Guardando...' : 'Crear reserva'} {isCreatingBooking
? isRecurring
? 'Creando turnos...'
: 'Guardando...'
: isRecurring
? 'Crear turno fijo'
: 'Crear reserva'}
</Button> </Button>
</ResponsiveDialogFooter> </ResponsiveDialogFooter>
</ResponsiveDialogContent> </ResponsiveDialogContent>
</ResponsiveDialog> </ResponsiveDialog>
); );
} }
function getDayOfWeekLabel(dateIso: string): string {
const labels: Record<string, string> = {
MONDAY: 'lunes',
TUESDAY: 'martes',
WEDNESDAY: 'miércoles',
THURSDAY: 'jueves',
FRIDAY: 'viernes',
SATURDAY: 'sábado',
SUNDAY: 'domingo',
};
return labels[getDayOfWeek(dateIso)] ?? '';
}

View File

@@ -1,4 +1,4 @@
import { CalendarDays, Download, ShieldCheck, UsersRound } from 'lucide-react'; import { CalendarDays, Download, Repeat, ShieldCheck, UsersRound } from 'lucide-react';
import { useBooking } from '../booking-provider'; import { useBooking } from '../booking-provider';
import { formatBookingDate, toIsoDateLocal } from '../lib/booking-time'; import { formatBookingDate, toIsoDateLocal } from '../lib/booking-time';
@@ -66,10 +66,8 @@ function SummaryCard({ label, value, detail, icon: Icon, className }: SummaryCar
} }
export function BookingQuickActions() { export function BookingQuickActions() {
const { selectedDate, selectedStatus, setSelectedDate, setSelectedStatus, exportDayReport } = const { setSelectedDate, setViewMode, exportDayReport, isRecurringEnabled } = useBooking();
useBooking();
const todayIso = toIsoDateLocal(new Date()); const todayIso = toIsoDateLocal(new Date());
const isViewingTodayReservations = selectedDate === todayIso && selectedStatus === 'reserved';
return ( return (
<section className="rounded-lg border bg-card/85 p-5 shadow-sm"> <section className="rounded-lg border bg-card/85 p-5 shadow-sm">
@@ -79,13 +77,12 @@ export function BookingQuickActions() {
type="button" type="button"
onClick={() => { onClick={() => {
setSelectedDate(todayIso); setSelectedDate(todayIso);
setSelectedStatus(isViewingTodayReservations ? 'all' : 'reserved'); setViewMode('list');
}} }}
aria-pressed={isViewingTodayReservations} className="flex h-10 items-center gap-3 rounded-md border bg-background/45 px-3 text-left text-sm transition-colors hover:bg-muted"
className="flex h-10 items-center gap-3 rounded-md border bg-background/45 px-3 text-left text-sm transition-colors hover:bg-muted aria-pressed:border-reserved/50 aria-pressed:bg-reserved/10"
> >
<CalendarDays className="size-4 text-muted-foreground" /> <CalendarDays className="size-4 text-muted-foreground" />
{isViewingTodayReservations ? 'Ver todos los estados' : 'Ver reservas de hoy'} Ver reservas de hoy
</button> </button>
<button <button
type="button" type="button"
@@ -95,6 +92,16 @@ export function BookingQuickActions() {
<Download className="size-4 text-primary" /> <Download className="size-4 text-primary" />
Exportar reporte Exportar reporte
</button> </button>
{isRecurringEnabled && (
<button
type="button"
onClick={() => setViewMode('recurring')}
className="flex h-10 items-center gap-3 rounded-md border bg-background/45 px-3 text-left text-sm transition-colors hover:bg-muted"
>
<Repeat className="size-4 text-muted-foreground" />
Ver turnos fijos
</button>
)}
</div> </div>
</section> </section>
); );

View File

@@ -0,0 +1,249 @@
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
import {
ArrowLeft,
CalendarDays,
Clock,
MapPin,
Phone,
ShieldCheck,
User,
UserX,
} from 'lucide-react';
import { useMemo } from 'react';
import { useBooking } from '../booking-provider';
import type { BookingTimelineSegment } from '../booking.types';
import { formatBookingDate } from '../lib/booking-time';
const statusConfig: Record<string, { label: string; className: string }> = {
CONFIRMED: {
label: 'Reservada',
className: 'bg-emerald-500/15 text-emerald-600 dark:text-emerald-400 border-emerald-500/30',
},
COMPLETED: {
label: 'Completada',
className: 'bg-sky-500/15 text-sky-600 dark:text-sky-400 border-sky-500/30',
},
CANCELLED: {
label: 'Cancelada',
className: 'bg-red-500/15 text-red-600 dark:text-red-400 border-red-500/30',
},
NOSHOW: {
label: 'No show',
className: 'bg-amber-500/15 text-amber-600 dark:text-amber-400 border-amber-500/30',
},
};
const statusIconConfig: Record<string, typeof ShieldCheck> = {
CONFIRMED: ShieldCheck,
COMPLETED: ShieldCheck,
CANCELLED: UserX,
NOSHOW: UserX,
};
export function BookingListView() {
const {
selectedDate,
setViewMode,
schedules,
openBookingTools,
isLoading,
isError,
errorMessage,
} = useBooking();
const reservedSegments = useMemo(() => {
return schedules
.flatMap((schedule) =>
schedule.segments
.filter((segment) => segment.booking)
.map((segment) => ({ schedule, segment }))
)
.sort((a, b) => {
const timeDiff = a.segment.startMinutes - b.segment.startMinutes;
if (timeDiff !== 0) return timeDiff;
return a.schedule.court.name.localeCompare(b.schedule.court.name);
});
}, [schedules]);
return (
<section className="rounded-lg border bg-card/85 shadow-sm">
<div className="flex flex-wrap items-center justify-between gap-4 border-b px-4 py-4">
<div className="flex items-center gap-3">
<Button
type="button"
variant="ghost"
size="icon"
className="rounded-full"
onClick={() => setViewMode('panel')}
>
<ArrowLeft className="size-5" />
</Button>
<div>
<h2 className="text-base font-semibold">Reservas del día</h2>
<p className="text-sm capitalize text-muted-foreground">
{formatBookingDate(selectedDate, { year: 'numeric' })}
</p>
</div>
</div>
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<CalendarDays className="size-4" />
<span>
{reservedSegments.length} {reservedSegments.length === 1 ? 'reserva' : 'reservas'}
</span>
</div>
</div>
{isLoading && (
<div className="px-4 py-10 text-sm text-muted-foreground">Cargando reservas...</div>
)}
{isError && !isLoading && (
<div className="px-4 py-10 text-sm text-destructive">{errorMessage}</div>
)}
{!isLoading && !isError && reservedSegments.length === 0 && (
<div className="px-4 py-10 text-center text-sm text-muted-foreground">
<CalendarDays className="mx-auto mb-3 size-10 opacity-40" />
<p>No hay reservas para este día.</p>
</div>
)}
{!isLoading && !isError && reservedSegments.length > 0 && (
<div className="overflow-x-auto">
{/* Desktop table */}
<table className="hidden w-full sm:table">
<thead>
<tr className="border-b text-left text-xs font-medium uppercase tracking-wider text-muted-foreground">
<th className="px-4 py-3 font-medium">Horario</th>
<th className="px-4 py-3 font-medium">Cancha</th>
<th className="px-4 py-3 font-medium">Cliente</th>
<th className="hidden px-4 py-3 font-medium md:table-cell">Teléfono</th>
<th className="px-4 py-3 font-medium">Estado</th>
</tr>
</thead>
<tbody className="divide-y">
{reservedSegments.map(({ schedule, segment }) => (
<BookingRow
key={segment.id}
segment={segment}
courtName={schedule.court.name}
sportName={schedule.court.sport.name}
onClick={() => openBookingTools(segment)}
/>
))}
</tbody>
</table>
{/* Mobile cards */}
<div className="divide-y sm:hidden">
{reservedSegments.map(({ schedule, segment }) => (
<button
key={segment.id}
type="button"
className="flex w-full items-center gap-4 px-4 py-4 text-left transition-colors hover:bg-muted/50"
onClick={() => openBookingTools(segment)}
>
<div className="flex size-12 shrink-0 items-center justify-center rounded-lg border border-border/60 bg-secondary/40 text-muted-foreground">
<Clock className="size-5" />
</div>
<div className="min-w-0 flex-1">
<p className="truncate text-sm font-semibold">
{segment.booking?.customerName ?? 'Sin información'}
</p>
<p className="mt-0.5 text-xs text-muted-foreground">
{schedule.court.name} · {schedule.court.sport.name}
</p>
<p className="mt-0.5 text-xs text-muted-foreground">
{segment.startTime} - {segment.endTime}
</p>
</div>
<StatusBadge status={segment.booking?.status ?? 'CONFIRMED'} />
</button>
))}
</div>
</div>
)}
</section>
);
}
interface BookingRowProps {
segment: BookingTimelineSegment;
courtName: string;
sportName: string;
onClick: () => void;
}
function BookingRow({ segment, courtName, sportName, onClick }: BookingRowProps) {
const booking = segment.booking;
return (
<tr
className="cursor-pointer transition-colors hover:bg-muted/50"
onClick={onClick}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
onClick();
}
}}
tabIndex={0}
role="button"
>
<td className="px-4 py-4">
<div className="flex items-center gap-2">
<Clock className="size-4 shrink-0 text-muted-foreground" />
<span className="text-sm font-medium">
{segment.startTime} - {segment.endTime}
</span>
</div>
</td>
<td className="px-4 py-4">
<div className="flex items-center gap-2">
<MapPin className="size-4 shrink-0 text-muted-foreground" />
<div className="min-w-0">
<p className="truncate text-sm font-medium">{courtName}</p>
<p className="truncate text-xs text-muted-foreground">{sportName}</p>
</div>
</div>
</td>
<td className="px-4 py-4">
<div className="flex items-center gap-2">
<User className="size-4 shrink-0 text-muted-foreground" />
<span className="truncate text-sm">{booking?.customerName ?? 'Sin información'}</span>
</div>
</td>
<td className="hidden px-4 py-4 md:table-cell">
<div className="flex items-center gap-2">
<Phone className="size-4 shrink-0 text-muted-foreground" />
<span className="text-sm text-muted-foreground">{booking?.customerPhone ?? '-'}</span>
</div>
</td>
<td className="px-4 py-4">
<StatusBadge status={booking?.status ?? 'CONFIRMED'} />
</td>
</tr>
);
}
function StatusBadge({ status }: { status: string }) {
const config = statusConfig[status] ?? {
label: status,
className: 'bg-slate-500/15 text-slate-600 dark:text-slate-400 border-slate-500/30',
};
const Icon = statusIconConfig[status] ?? ShieldCheck;
return (
<span
className={cn(
'inline-flex items-center gap-1.5 rounded-full border px-2.5 py-1 text-xs font-semibold',
config.className
)}
>
<Icon className="size-3" />
{config.label}
</span>
);
}

View File

@@ -0,0 +1,210 @@
import { Button } from '@/components/ui/button';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { apiClient } from '@/lib/api-client';
import type { RecurringBookingGroup } from '@repo/api-contract';
import { useEffect, useState } from 'react';
import { useBooking } from '../booking-provider';
import { minutesToTime, timeToMinutes } from '../lib/booking-time';
const DAY_OPTIONS = [
{ value: 'MONDAY', label: 'Lunes' },
{ value: 'TUESDAY', label: 'Martes' },
{ value: 'WEDNESDAY', label: 'Miércoles' },
{ value: 'THURSDAY', label: 'Jueves' },
{ value: 'FRIDAY', label: 'Viernes' },
{ value: 'SATURDAY', label: 'Sábado' },
{ value: 'SUNDAY', label: 'Domingo' },
];
interface RecurringBookingEditDialogProps {
group: RecurringBookingGroup;
open: boolean;
onClose: () => void;
}
export function RecurringBookingEditDialog({
group,
open,
onClose,
}: RecurringBookingEditDialogProps) {
const { courts, refreshRecurringGroups } = useBooking();
const [courtId, setCourtId] = useState<string>(group.courtId);
const [dayOfWeek, setDayOfWeek] = useState<string>(group.dayOfWeek);
const [startTime, setStartTime] = useState<string>(group.startTime);
const [customerName, setCustomerName] = useState(group.customerName);
const [customerPhone, setCustomerPhone] = useState(group.customerPhone);
const [customerEmail, setCustomerEmail] = useState(group.customerEmail);
const [error, setError] = useState('');
const [saving, setSaving] = useState(false);
useEffect(() => {
setCourtId(group.courtId);
setDayOfWeek(group.dayOfWeek);
setStartTime(group.startTime);
setCustomerName(group.customerName);
setCustomerPhone(group.customerPhone);
setCustomerEmail(group.customerEmail);
}, [group]);
const selectedCourt = courts.find((c) => c.id === courtId);
const slotDuration = selectedCourt?.slotDurationMinutes ?? 60;
const availableSlots: string[] = [];
if (selectedCourt) {
const availabilities = selectedCourt.availability.filter((a) => a.dayOfWeek === dayOfWeek);
for (const avail of availabilities) {
const startMins = timeToMinutes(avail.startTime);
const endMins = timeToMinutes(avail.endTime);
for (let m = startMins; m + slotDuration <= endMins; m += slotDuration) {
availableSlots.push(minutesToTime(m));
}
}
}
const handleSave = async () => {
if (!customerName || customerName.trim().length < 2) {
setError('El nombre debe tener al menos 2 caracteres.');
return;
}
setError('');
setSaving(true);
try {
await apiClient.adminBookings.updateRecurringGroup(group.id, {
...(courtId !== group.courtId && { courtId }),
...(dayOfWeek !== group.dayOfWeek && {
dayOfWeek: dayOfWeek as
| 'MONDAY'
| 'TUESDAY'
| 'WEDNESDAY'
| 'THURSDAY'
| 'FRIDAY'
| 'SATURDAY'
| 'SUNDAY',
}),
...(startTime !== group.startTime && { startTime }),
...(customerName !== group.customerName && { customerName }),
...(customerPhone !== group.customerPhone && { customerPhone }),
...(customerEmail !== group.customerEmail && { customerEmail }),
});
await refreshRecurringGroups();
onClose();
} catch (e) {
setError(e instanceof Error ? e.message : 'Error al guardar los cambios.');
} finally {
setSaving(false);
}
};
return (
<Dialog open={open} onOpenChange={(open) => !open && onClose()}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>Editar turno fijo</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-1.5">
<Label>Cliente</Label>
<Input
value={customerName}
onChange={(e) => setCustomerName(e.target.value)}
placeholder="Nombre del cliente"
/>
</div>
<div className="space-y-1.5">
<Label>Teléfono</Label>
<Input
value={customerPhone}
onChange={(e) => setCustomerPhone(e.target.value)}
placeholder="Teléfono"
/>
</div>
<div className="space-y-1.5">
<Label>Email</Label>
<Input
value={customerEmail}
onChange={(e) => setCustomerEmail(e.target.value)}
placeholder="Email"
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div className="space-y-1.5">
<Label>Cancha</Label>
<Select value={courtId} onValueChange={setCourtId}>
<SelectTrigger>
<SelectValue placeholder="Cancha" />
</SelectTrigger>
<SelectContent>
{courts.map((court) => (
<SelectItem key={court.id} value={court.id}>
{court.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-1.5">
<Label>Día</Label>
<Select value={dayOfWeek} onValueChange={setDayOfWeek}>
<SelectTrigger>
<SelectValue placeholder="Día" />
</SelectTrigger>
<SelectContent>
{DAY_OPTIONS.map((day) => (
<SelectItem key={day.value} value={day.value}>
{day.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-1.5">
<Label>Horario</Label>
<Select value={startTime} onValueChange={setStartTime}>
<SelectTrigger>
<SelectValue placeholder="Horario" />
</SelectTrigger>
<SelectContent>
{availableSlots.map((slot) => (
<SelectItem key={slot} value={slot}>
{slot} - {minutesToTime(timeToMinutes(slot) + slotDuration)}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{error && <p className="text-sm text-destructive">{error}</p>}
<div className="flex justify-end gap-3 pt-2">
<Button type="button" variant="outline" onClick={onClose}>
Cancelar
</Button>
<Button type="button" onClick={handleSave} disabled={saving}>
{saving ? 'Guardando...' : 'Guardar cambios'}
</Button>
</div>
</div>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,246 @@
import { Button } from '@/components/ui/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import type { RecurringBookingGroup } from '@repo/api-contract';
import { ArrowLeft, Clock, MapPin, Pencil, Repeat, Trash2, User } from 'lucide-react';
import { useState } from 'react';
import { useBooking } from '../booking-provider';
import { RecurringBookingEditDialog } from './recurring-booking-edit-dialog';
const DAY_LABELS: Record<string, string> = {
MONDAY: 'Lunes',
TUESDAY: 'Martes',
WEDNESDAY: 'Miércoles',
THURSDAY: 'Jueves',
FRIDAY: 'Viernes',
SATURDAY: 'Sábado',
SUNDAY: 'Domingo',
};
function getNextBookingDate(group: RecurringBookingGroup): string {
const today = new Date().toISOString().slice(0, 10);
const future = group.bookings
.filter((b) => b.date >= today && b.status === 'CONFIRMED')
.sort((a, b) => a.date.localeCompare(b.date));
return future[0]?.date ?? '-';
}
function formatDate(dateStr: string): string {
if (dateStr === '-') return '-';
const [y, m, d] = dateStr.split('-');
return `${d}/${m}/${y}`;
}
export function RecurringBookingListView() {
const {
setViewMode,
recurringGroups,
isRecurringGroupsLoading,
cancelRecurringGroup,
isCancellingRecurringGroup,
} = useBooking();
const [editingGroup, setEditingGroup] = useState<RecurringBookingGroup | null>(null);
const [cancellingGroupId, setCancellingGroupId] = useState<string | null>(null);
return (
<>
<section className="rounded-lg border bg-card/85 shadow-sm">
<div className="flex flex-wrap items-center justify-between gap-4 border-b px-4 py-4">
<div className="flex items-center gap-3">
<Button
type="button"
variant="ghost"
size="icon"
className="rounded-full"
onClick={() => setViewMode('panel')}
>
<ArrowLeft className="size-5" />
</Button>
<div>
<h2 className="text-base font-semibold">Turnos fijos</h2>
<p className="text-sm text-muted-foreground">
{recurringGroups.length}{' '}
{recurringGroups.length === 1 ? 'turno fijo' : 'turnos fijos'}
</p>
</div>
</div>
</div>
{isRecurringGroupsLoading && (
<div className="px-4 py-10 text-sm text-muted-foreground">Cargando turnos fijos...</div>
)}
{!isRecurringGroupsLoading && recurringGroups.length === 0 && (
<div className="px-4 py-10 text-center text-sm text-muted-foreground">
<Repeat className="mx-auto mb-3 size-10 opacity-40" />
<p>No hay turnos fijos creados.</p>
</div>
)}
{!isRecurringGroupsLoading && recurringGroups.length > 0 && (
<div className="overflow-x-auto">
<table className="hidden w-full sm:table">
<thead>
<tr className="border-b text-left text-xs font-medium uppercase tracking-wider text-muted-foreground">
<th className="px-4 py-3 font-medium">Cliente</th>
<th className="px-4 py-3 font-medium">Cancha</th>
<th className="px-4 py-3 font-medium">Día</th>
<th className="px-4 py-3 font-medium">Horario</th>
<th className="px-4 py-3 font-medium">Próxima fecha</th>
<th className="px-4 py-3 font-medium">Acciones</th>
</tr>
</thead>
<tbody className="divide-y">
{recurringGroups.map((group) => {
const nextDate = getNextBookingDate(group);
return (
<tr key={group.id} className="transition-colors hover:bg-muted/50">
<td className="px-4 py-4">
<div className="flex items-center gap-2">
<User className="size-4 shrink-0 text-muted-foreground" />
<span className="truncate text-sm">{group.customerName || '-'}</span>
</div>
</td>
<td className="px-4 py-4">
<div className="flex items-center gap-2">
<MapPin className="size-4 shrink-0 text-muted-foreground" />
<span className="truncate text-sm">
{group.bookings[0]?.courtName ?? '-'}
</span>
</div>
</td>
<td className="px-4 py-4 text-sm">
{DAY_LABELS[group.dayOfWeek] ?? group.dayOfWeek}
</td>
<td className="px-4 py-4">
<div className="flex items-center gap-2">
<Clock className="size-4 shrink-0 text-muted-foreground" />
<span className="text-sm">
{group.startTime} - {group.endTime}
</span>
</div>
</td>
<td className="px-4 py-4 text-sm">{formatDate(nextDate)}</td>
<td className="px-4 py-4">
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => setEditingGroup(group)}
className="flex items-center gap-1.5 rounded-md border px-2.5 py-1.5 text-xs font-medium transition-colors hover:bg-muted"
>
<Pencil className="size-3.5" />
Editar
</button>
<button
type="button"
onClick={() => setCancellingGroupId(group.id)}
disabled={isCancellingRecurringGroup}
className="flex items-center gap-1.5 rounded-md border border-red-200 px-2.5 py-1.5 text-xs font-medium text-red-600 transition-colors hover:bg-red-50 disabled:opacity-50"
>
<Trash2 className="size-3.5" />
Cancelar
</button>
</div>
</td>
</tr>
);
})}
</tbody>
</table>
<div className="divide-y sm:hidden">
{recurringGroups.map((group) => {
const nextDate = getNextBookingDate(group);
return (
<div key={group.id} className="px-4 py-4">
<div className="mb-2 flex items-center justify-between">
<div className="flex items-center gap-2">
<User className="size-4 text-muted-foreground" />
<span className="text-sm font-semibold">{group.customerName || '-'}</span>
</div>
<span className="rounded-full bg-primary/10 px-2 py-0.5 text-xs font-medium text-primary">
{DAY_LABELS[group.dayOfWeek]}
</span>
</div>
<p className="text-xs text-muted-foreground">
{group.bookings[0]?.courtName} · {group.startTime} - {group.endTime}
</p>
<p className="mt-1 text-xs text-muted-foreground">
Próxima: {formatDate(nextDate)}
</p>
<div className="mt-2 flex gap-2">
<button
type="button"
onClick={() => setEditingGroup(group)}
className="flex items-center gap-1 rounded-md border px-2 py-1 text-xs transition-colors hover:bg-muted"
>
<Pencil className="size-3" />
Editar
</button>
<button
type="button"
onClick={() => setCancellingGroupId(group.id)}
disabled={isCancellingRecurringGroup}
className="flex items-center gap-1 rounded-md border border-red-200 px-2 py-1 text-xs text-red-600 transition-colors hover:bg-red-50 disabled:opacity-50"
>
<Trash2 className="size-3" />
Cancelar
</button>
</div>
</div>
);
})}
</div>
</div>
)}
</section>
<Dialog
open={!!cancellingGroupId}
onOpenChange={(open) => !open && setCancellingGroupId(null)}
>
<DialogContent>
<DialogHeader>
<DialogTitle>Cancelar turno fijo</DialogTitle>
<DialogDescription>
¿Estás seguro de cancelar este turno fijo? Se eliminarán todas las reservas futuras.
Las reservas pasadas no se verán afectadas.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={() => setCancellingGroupId(null)}>
Volver
</Button>
<Button
variant="destructive"
disabled={isCancellingRecurringGroup}
onClick={() => {
if (cancellingGroupId) {
cancelRecurringGroup(cancellingGroupId);
setCancellingGroupId(null);
}
}}
>
{isCancellingRecurringGroup ? 'Cancelando...' : 'Sí, cancelar turno fijo'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{editingGroup && (
<RecurringBookingEditDialog
group={editingGroup}
open={!!editingGroup}
onClose={() => setEditingGroup(null)}
/>
)}
</>
);
}

View File

@@ -13,6 +13,7 @@ const FEATURE_LABELS: Record<string, string> = {
onlinePayments: 'Pagos online', onlinePayments: 'Pagos online',
advancedReports: 'Reportes avanzados', advancedReports: 'Reportes avanzados',
whatsappReminders: 'Recordatorios WhatsApp', whatsappReminders: 'Recordatorios WhatsApp',
fixedSlots: 'Turnos fijos',
}; };
export function PlanSelectStep({ form, plans }: PlanSelectStepProps) { export function PlanSelectStep({ form, plans }: PlanSelectStepProps) {
@@ -24,6 +25,7 @@ export function PlanSelectStep({ form, plans }: PlanSelectStepProps) {
'onlinePayments', 'onlinePayments',
'advancedReports', 'advancedReports',
'whatsappReminders', 'whatsappReminders',
'fixedSlots',
] as const; ] as const;
return ( return (
@@ -58,7 +60,11 @@ export function PlanSelectStep({ form, plans }: PlanSelectStepProps) {
<div className="mb-3"> <div className="mb-3">
<h3 className="text-xl font-bold">{plan.name}</h3> <h3 className="text-xl font-bold">{plan.name}</h3>
<div className="mt-1"> <div className="mt-1">
<span className="text-3xl font-bold">${plan.price}</span> <span className="text-3xl font-bold">
{plan.currency === 'ARS'
? `AR$${plan.price.toLocaleString('es-AR')}`
: `$${plan.price.toFixed(2)}`}
</span>
<span className="text-sm text-muted-foreground">/mes</span> <span className="text-sm text-muted-foreground">/mes</span>
</div> </div>
</div> </div>

View File

@@ -50,7 +50,14 @@ export function ReviewStep({ data, selectedPlan }: ReviewStepProps) {
<Section title="Plan seleccionado"> <Section title="Plan seleccionado">
{selectedPlan ? ( {selectedPlan ? (
<> <>
<Row label="Plan" value={`${selectedPlan.name} - $${selectedPlan.price}/mes`} /> <Row
label="Plan"
value={`${selectedPlan.name} - ${
selectedPlan.currency === 'ARS'
? `AR$${selectedPlan.price.toLocaleString('es-AR')}`
: `$${selectedPlan.price.toFixed(2)}`
}/mes`}
/>
<div className="mt-2 space-y-1"> <div className="mt-2 space-y-1">
{[ {[
['publicBookingPage', 'Página de booking pública'], ['publicBookingPage', 'Página de booking pública'],

View File

@@ -26,7 +26,6 @@ import {
Check, Check,
ChevronLeft, ChevronLeft,
ChevronRight, ChevronRight,
Clock3,
Dumbbell, Dumbbell,
Lock, Lock,
MapPin, MapPin,
@@ -34,7 +33,6 @@ import {
Moon, Moon,
Share2, Share2,
ShieldCheck, ShieldCheck,
Shirt,
Sparkles, Sparkles,
Sun, Sun,
Trophy, Trophy,
@@ -414,7 +412,7 @@ function PublicBookingPageChrome({
if (mobile) { if (mobile) {
return ( return (
<main className={`${themeClass} min-h-screen bg-[#050d14] text-white`}> <main className={`${themeClass} min-h-screen bg-[#050d14] text-white`}>
<div className="public-booking-frame mx-auto min-h-screen max-w-[430px] bg-[radial-gradient(circle_at_50%_0%,rgba(17,185,129,0.16),transparent_34%),linear-gradient(180deg,#07131d_0%,#071018_47%,#050b11_100%)] px-4 pb-24 pt-7 shadow-2xl shadow-black"> <div className="public-booking-frame mx-auto min-h-screen max-w-[430px] bg-[radial-gradient(circle_at_50%_0%,rgba(17,185,129,0.16),transparent_34%),linear-gradient(180deg,#07131d_0%,#071018_47%,#050b11_100%)] px-4 pb-0 pt-7 shadow-2xl shadow-black">
<header className="flex items-center justify-between"> <header className="flex items-center justify-between">
<PlayzerBrand compact /> <PlayzerBrand compact />
<PublicBookingThemeToggle /> <PublicBookingThemeToggle />
@@ -441,8 +439,6 @@ function PublicBookingPageChrome({
</div> </div>
{children} {children}
<MobileBottomNav />
</div> </div>
</main> </main>
); );
@@ -553,8 +549,7 @@ function PublicBookingDesktop(props: BookingShellProps) {
const currentStep = getStep(selectedSlot, props.form.formState.isValid); const currentStep = getStep(selectedSlot, props.form.formState.isValid);
const completedSteps = getCompletedSteps(selectedSlot, props.form.formState.isValid); const completedSteps = getCompletedSteps(selectedSlot, props.form.formState.isValid);
const selectedDay = dayOptions.find((option) => option.value === selectedDate); const selectedDay = dayOptions.find((option) => option.value === selectedDate);
const currentDayIndex = dayOptions.findIndex((option) => option.value === selectedDate); const canGoBackButton = canGoBack;
const canGoBackButton = canGoBack || currentDayIndex > 0;
return ( return (
<PublicBookingPageChrome <PublicBookingPageChrome
@@ -683,10 +678,7 @@ function PublicBookingDesktop(props: BookingShellProps) {
</div> </div>
</Panel> </Panel>
<div className="grid grid-cols-[minmax(0,1fr)_minmax(340px,0.95fr)] gap-4">
<CourtDetails court={selectedCourt} />
<SelectedSlotCard {...props} /> <SelectedSlotCard {...props} />
</div>
</> </>
)} )}
</section> </section>
@@ -1161,41 +1153,6 @@ function BookingTimeline({
); );
} }
function CourtDetails({ court }: { court?: PublicAvailabilityCourt }) {
return (
<Panel className="p-6">
<div className="flex items-center gap-6">
<div className="hidden size-24 items-center justify-center rounded border border-white/16 text-white/45 md:flex">
<MapPin className="size-14" />
</div>
<div>
<h3 className="text-lg font-semibold">
{court?.courtName ? `${court.courtName} de ${court.sport.name}` : 'Cancha'}
</h3>
<div className="mt-4 space-y-2 text-sm text-white/62">
<p className="flex items-center gap-2">
<Users className="size-4 text-emerald-400" />
Superficie: Césped sintético
</p>
<p className="flex items-center gap-2">
<Dumbbell className="size-4 text-emerald-400" />
Turnos de {court?.slotDurationMinutes ?? 60} minutos
</p>
<p className="flex items-center gap-2">
<Sparkles className="size-4 text-emerald-400" />
Iluminación LED
</p>
<p className="flex items-center gap-2">
<Shirt className="size-4 text-emerald-400" />
Vestuarios disponibles
</p>
</div>
</div>
</div>
</Panel>
);
}
function SelectedSlotCard(props: BookingShellProps & { compact?: boolean }) { function SelectedSlotCard(props: BookingShellProps & { compact?: boolean }) {
const { const {
selectedSlot, selectedSlot,
@@ -1217,9 +1174,11 @@ function SelectedSlotCard(props: BookingShellProps & { compact?: boolean }) {
useEffect(() => { useEffect(() => {
if (!selectedSlot) return; if (!selectedSlot) return;
window.requestAnimationFrame(() => { const input = customerNameInputRef.current;
customerNameInputRef.current?.focus(); if (!input) return;
});
input.focus();
input.scrollIntoView({ behavior: 'smooth', block: 'center' });
}, [selectedSlot]); }, [selectedSlot]);
return ( return (
@@ -1322,40 +1281,6 @@ function SelectedSlotCard(props: BookingShellProps & { compact?: boolean }) {
); );
} }
function MobileBottomNav() {
const items = [
{ label: 'Reservar', icon: CalendarDays, active: true },
{ label: 'Canchas', icon: Building2 },
{ label: 'Cómo funciona', icon: Clock3 },
{ label: 'Contacto', icon: Users },
];
return (
<nav className="fixed inset-x-0 bottom-0 z-40 mx-auto max-w-[430px] border-t border-emerald-500/30 bg-[#061019]/94 px-4 py-3 backdrop-blur">
<div className="grid grid-cols-4 gap-1">
{items.map((item) => {
const Icon = item.icon;
return (
<a
key={item.label}
href={
item.label === 'Reservar' ? '#' : `#${item.label.toLowerCase().replace(' ', '-')}`
}
className={`flex flex-col items-center gap-1 text-[11px] ${
item.active ? 'text-emerald-400' : 'text-white/58'
}`}
>
<Icon className="size-5" />
{item.label}
</a>
);
})}
</div>
</nav>
);
}
export function PublicBookingPage({ complexSlug }: PublicBookingPageProps) { export function PublicBookingPage({ complexSlug }: PublicBookingPageProps) {
const navigate = useNavigate(); const navigate = useNavigate();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
@@ -1579,7 +1504,8 @@ export function PublicBookingPage({ complexSlug }: PublicBookingPageProps) {
visibleCourts.length, visibleCourts.length,
]); ]);
const canGoBack = windowStartOffset > 0; const currentDayIndex = dayOptions.findIndex((option) => option.value === selectedDate);
const canGoBack = windowStartOffset > 0 || currentDayIndex > 0;
const selectDate = (date: string) => { const selectDate = (date: string) => {
setSelectedDate(date); setSelectedDate(date);

View File

@@ -5,24 +5,40 @@ import * as api from './api';
const apiBaseUrl = api.apiBaseUrl; const apiBaseUrl = api.apiBaseUrl;
const plugins = [ const infraPlugins = import.meta.env.VITE_BETTER_AUTH_INFRA === 'true' ? [sentinelClient()] : [];
sentinelClient(),
inferAdditionalFields({
user: {
phone: {
type: 'string',
required: false,
},
},
}),
];
export const authClient = createAuthClient({ export const authClient = createAuthClient({
baseURL: apiBaseUrl, baseURL: apiBaseUrl,
fetchOptions: { fetchOptions: {
credentials: 'include', credentials: 'include',
}, },
plugins: import.meta.env.VITE_BETTER_AUTH_INFRA === 'true' ? plugins : [], plugins: [
...infraPlugins,
inferAdditionalFields({
user: {
phone: {
type: 'string',
required: false,
},
role: {
type: 'string',
required: false,
},
banned: {
type: 'boolean',
required: false,
},
bannedAt: {
type: 'string',
required: false,
},
banReason: {
type: 'string',
required: false,
},
},
}),
],
}); });
export class ApiClientError extends Error { export class ApiClientError extends Error {
@@ -48,6 +64,7 @@ export const apiClient = {
complexes: api.complexes, complexes: api.complexes,
sports: api.sports, sports: api.sports,
courts: api.courts, courts: api.courts,
admin: api.admin,
publicBookings: { publicBookings: {
getAvailability: api.getAvailability, getAvailability: api.getAvailability,
create: api.createPublic, create: api.createPublic,
@@ -57,6 +74,10 @@ export const apiClient = {
adminBookings: { adminBookings: {
listByComplex: api.listByComplex, listByComplex: api.listByComplex,
create: api.createAdmin, create: api.createAdmin,
createRecurring: api.createRecurring,
cancelRecurringGroup: api.cancelRecurringGroup,
listRecurringGroups: api.listRecurringGroups,
updateRecurringGroup: api.updateRecurringGroup,
updateStatus: api.updateStatus, updateStatus: api.updateStatus,
}, },
}; };

View File

@@ -17,6 +17,7 @@ export class ApiClientError extends Error {
export type ErrorHandlers = { export type ErrorHandlers = {
onForbidden?: (error: ApiClientError) => void | Promise<void>; onForbidden?: (error: ApiClientError) => void | Promise<void>;
onUnauthorized?: (error: ApiClientError) => void | Promise<void>;
onError?: (error: ApiClientError) => void | Promise<void>; onError?: (error: ApiClientError) => void | Promise<void>;
}; };
@@ -28,6 +29,7 @@ export const apiBaseUrl =
export function configureApiClient(nextHandlers: ErrorHandlers) { export function configureApiClient(nextHandlers: ErrorHandlers) {
handlers.onForbidden = nextHandlers.onForbidden; handlers.onForbidden = nextHandlers.onForbidden;
handlers.onUnauthorized = nextHandlers.onUnauthorized;
handlers.onError = nextHandlers.onError; handlers.onError = nextHandlers.onError;
} }

View File

@@ -69,6 +69,10 @@ http.interceptors.response.use(
async (error: AxiosError) => { async (error: AxiosError) => {
const normalized = normalizeError(error); const normalized = normalizeError(error);
if (normalized.status === 401 && handlers.onUnauthorized) {
await handlers.onUnauthorized(normalized);
}
if (normalized.status === 403 && handlers.onForbidden) { if (normalized.status === 403 && handlers.onForbidden) {
await handlers.onForbidden(normalized); await handlers.onForbidden(normalized);
} }

View File

@@ -6,6 +6,7 @@ export * as courts from './resources/courts';
export * as user from './resources/user'; export * as user from './resources/user';
export * as sports from './resources/sports'; export * as sports from './resources/sports';
export * as plans from './resources/plans'; export * as plans from './resources/plans';
export * as admin from './resources/admin';
export { export {
cancelPublic, cancelPublic,
@@ -14,5 +15,9 @@ export {
getConfirmation, getConfirmation,
listByComplex, listByComplex,
createAdmin, createAdmin,
createRecurring,
cancelRecurringGroup,
listRecurringGroups,
updateRecurringGroup,
updateStatus, updateStatus,
} from './resources/bookings'; } from './resources/bookings';

View File

@@ -0,0 +1,96 @@
import type {
AdminBlockUserInput,
AdminComplexListItem,
AdminCreatePlanInput,
AdminGeoStats,
AdminGlobalStats,
AdminUpdatePlanInput,
AdminUser,
AdminUserSession,
} from '@repo/api-contract';
import { http } from '../http';
type AdminPlan = {
code: string;
name: string;
price: number;
rules: unknown;
lastUpdatedAt: string;
complexCount: number;
};
export async function listComplexes() {
const response = await http.get<AdminComplexListItem[]>('/api/admin/complexes');
return response.data;
}
export async function listPlans() {
const response = await http.get<AdminPlan[]>('/api/admin/plans');
return response.data;
}
export async function createPlan(data: AdminCreatePlanInput) {
const response = await http.post<{ code: string; name: string; price: number }>(
'/api/admin/plans',
JSON.stringify(data),
{ headers: { 'Content-Type': 'application/json' } }
);
return response.data;
}
export async function updatePlan(code: string, data: AdminUpdatePlanInput) {
const response = await http.patch<{ code: string; name: string; price: number }>(
`/api/admin/plans/${code}`,
JSON.stringify(data),
{ headers: { 'Content-Type': 'application/json' } }
);
return response.data;
}
export async function deletePlan(code: string) {
const response = await http.delete<{ message: string }>(`/api/admin/plans/${code}`);
return response.data;
}
export async function listUsers(search?: string) {
const response = await http.get<AdminUser[]>('/api/admin/users', {
params: search ? { search } : undefined,
});
return response.data;
}
export async function blockUser(userId: string, data?: AdminBlockUserInput) {
const response = await http.post<{ message: string }>(
`/api/admin/users/${userId}/block`,
JSON.stringify(data ?? {}),
{ headers: { 'Content-Type': 'application/json' } }
);
return response.data;
}
export async function unblockUser(userId: string) {
const response = await http.post<{ message: string }>(`/api/admin/users/${userId}/unblock`);
return response.data;
}
export async function getGlobalStats() {
const response = await http.get<AdminGlobalStats>('/api/admin/stats');
return response.data;
}
export async function getGeoStats() {
const response = await http.get<AdminGeoStats>('/api/admin/geo-stats');
return response.data;
}
export async function getUserSessions(userId: string) {
const response = await http.get<AdminUserSession[]>(`/api/admin/users/${userId}/sessions`);
return response.data;
}
export async function revokeAllSessions(userId: string) {
const response = await http.post<{ message: string }>(
`/api/admin/users/${userId}/sessions/revoke-all`
);
return response.data;
}

View File

@@ -3,11 +3,15 @@ import type {
CancelPublicBookingInput, CancelPublicBookingInput,
CreateAdminBookingInput, CreateAdminBookingInput,
CreatePublicBookingInput, CreatePublicBookingInput,
ListRecurringGroupsResponse,
PublicAvailabilityResponse, PublicAvailabilityResponse,
PublicBooking, PublicBooking,
PublicBookingConfirmation, PublicBookingConfirmation,
RecurringBookingGroup,
UpdateAdminBookingStatusInput, UpdateAdminBookingStatusInput,
UpdateRecurringGroupInput,
} from '@repo/api-contract'; } from '@repo/api-contract';
import type { CreateRecurringBookingInput } from '@repo/api-contract';
import { http } from '../http'; import { http } from '../http';
export async function getAvailability( export async function getAvailability(
@@ -52,6 +56,36 @@ export async function createAdmin(complexId: string, payload: CreateAdminBooking
return response.data; return response.data;
} }
export async function createRecurring(complexId: string, payload: CreateRecurringBookingInput) {
const response = await http.post<RecurringBookingGroup>(
`/api/admin-bookings/complex/${complexId}/recurring`,
payload
);
return response.data;
}
export async function cancelRecurringGroup(groupId: string) {
const response = await http.post<{ ok: boolean }>(
`/api/admin-bookings/recurring/${groupId}/cancel`
);
return response.data;
}
export async function listRecurringGroups(complexId: string) {
const response = await http.get<ListRecurringGroupsResponse>(
`/api/admin-bookings/complex/${complexId}/recurring`
);
return response.data;
}
export async function updateRecurringGroup(groupId: string, payload: UpdateRecurringGroupInput) {
const response = await http.patch<RecurringBookingGroup>(
`/api/admin-bookings/recurring/${groupId}`,
payload
);
return response.data;
}
export async function cancelPublic(complexSlug: string, payload: CancelPublicBookingInput) { export async function cancelPublic(complexSlug: string, payload: CancelPublicBookingInput) {
const response = await http.post<PublicBooking>( const response = await http.post<PublicBooking>(
`/api/public-bookings/complex/${complexSlug}/cancel`, `/api/public-bookings/complex/${complexSlug}/cancel`,

View File

@@ -27,6 +27,14 @@ function AppRouter() {
useEffect(() => { useEffect(() => {
configureApiClient({ configureApiClient({
onUnauthorized: async () => {
if (!auth.isAuthenticated) {
return;
}
await auth.signOut();
await router.navigate({ to: '/login' });
},
onForbidden: async (error) => { onForbidden: async (error) => {
if (error.message?.includes('verificar tu email')) { if (error.message?.includes('verificar tu email')) {
return; return;

View File

@@ -23,6 +23,11 @@ import { Route as ComplexSlugBookingRouteImport } from './routes/$complexSlug/bo
import { Route as AppAuthenticatedRouteRouteImport } from './routes/_app/_authenticated/route' import { Route as AppAuthenticatedRouteRouteImport } from './routes/_app/_authenticated/route'
import { Route as AppAuthenticatedIndexRouteImport } from './routes/_app/_authenticated/index' import { Route as AppAuthenticatedIndexRouteImport } from './routes/_app/_authenticated/index'
import { Route as ComplexSlugBookingIndexRouteImport } from './routes/$complexSlug/booking/index' import { Route as ComplexSlugBookingIndexRouteImport } from './routes/$complexSlug/booking/index'
import { Route as AppAuthenticatedAdminRouteRouteImport } from './routes/_app/_authenticated/admin/route'
import { Route as AppAuthenticatedAdminIndexRouteImport } from './routes/_app/_authenticated/admin/index'
import { Route as AppAuthenticatedAdminUsersRouteImport } from './routes/_app/_authenticated/admin/users'
import { Route as AppAuthenticatedAdminPlansRouteImport } from './routes/_app/_authenticated/admin/plans'
import { Route as AppAuthenticatedAdminComplexesRouteImport } from './routes/_app/_authenticated/admin/complexes'
import { Route as ComplexSlugBookingConfirmedBookingCodeRouteImport } from './routes/$complexSlug/booking/confirmed/$bookingCode' import { Route as ComplexSlugBookingConfirmedBookingCodeRouteImport } from './routes/$complexSlug/booking/confirmed/$bookingCode'
import { Route as AppAuthenticatedComplexSlugEditRouteImport } from './routes/_app/_authenticated/complex/$slug/edit' import { Route as AppAuthenticatedComplexSlugEditRouteImport } from './routes/_app/_authenticated/complex/$slug/edit'
@@ -94,6 +99,36 @@ const ComplexSlugBookingIndexRoute = ComplexSlugBookingIndexRouteImport.update({
path: '/', path: '/',
getParentRoute: () => ComplexSlugBookingRoute, getParentRoute: () => ComplexSlugBookingRoute,
} as any) } as any)
const AppAuthenticatedAdminRouteRoute =
AppAuthenticatedAdminRouteRouteImport.update({
id: '/admin',
path: '/admin',
getParentRoute: () => AppAuthenticatedRouteRoute,
} as any)
const AppAuthenticatedAdminIndexRoute =
AppAuthenticatedAdminIndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => AppAuthenticatedAdminRouteRoute,
} as any)
const AppAuthenticatedAdminUsersRoute =
AppAuthenticatedAdminUsersRouteImport.update({
id: '/users',
path: '/users',
getParentRoute: () => AppAuthenticatedAdminRouteRoute,
} as any)
const AppAuthenticatedAdminPlansRoute =
AppAuthenticatedAdminPlansRouteImport.update({
id: '/plans',
path: '/plans',
getParentRoute: () => AppAuthenticatedAdminRouteRoute,
} as any)
const AppAuthenticatedAdminComplexesRoute =
AppAuthenticatedAdminComplexesRouteImport.update({
id: '/complexes',
path: '/complexes',
getParentRoute: () => AppAuthenticatedAdminRouteRoute,
} as any)
const ComplexSlugBookingConfirmedBookingCodeRoute = const ComplexSlugBookingConfirmedBookingCodeRoute =
ComplexSlugBookingConfirmedBookingCodeRouteImport.update({ ComplexSlugBookingConfirmedBookingCodeRouteImport.update({
id: '/confirmed/$bookingCode', id: '/confirmed/$bookingCode',
@@ -119,8 +154,13 @@ export interface FileRoutesByFullPath {
'/$complexSlug/booking': typeof ComplexSlugBookingRouteWithChildren '/$complexSlug/booking': typeof ComplexSlugBookingRouteWithChildren
'/profile': typeof AppProfileRoute '/profile': typeof AppProfileRoute
'/onboard/setup': typeof OnboardSetupRoute '/onboard/setup': typeof OnboardSetupRoute
'/admin': typeof AppAuthenticatedAdminRouteRouteWithChildren
'/$complexSlug/booking/': typeof ComplexSlugBookingIndexRoute '/$complexSlug/booking/': typeof ComplexSlugBookingIndexRoute
'/$complexSlug/booking/confirmed/$bookingCode': typeof ComplexSlugBookingConfirmedBookingCodeRoute '/$complexSlug/booking/confirmed/$bookingCode': typeof ComplexSlugBookingConfirmedBookingCodeRoute
'/admin/complexes': typeof AppAuthenticatedAdminComplexesRoute
'/admin/plans': typeof AppAuthenticatedAdminPlansRoute
'/admin/users': typeof AppAuthenticatedAdminUsersRoute
'/admin/': typeof AppAuthenticatedAdminIndexRoute
'/complex/$slug/edit': typeof AppAuthenticatedComplexSlugEditRoute '/complex/$slug/edit': typeof AppAuthenticatedComplexSlugEditRoute
} }
export interface FileRoutesByTo { export interface FileRoutesByTo {
@@ -136,6 +176,10 @@ export interface FileRoutesByTo {
'/onboard/setup': typeof OnboardSetupRoute '/onboard/setup': typeof OnboardSetupRoute
'/$complexSlug/booking': typeof ComplexSlugBookingIndexRoute '/$complexSlug/booking': typeof ComplexSlugBookingIndexRoute
'/$complexSlug/booking/confirmed/$bookingCode': typeof ComplexSlugBookingConfirmedBookingCodeRoute '/$complexSlug/booking/confirmed/$bookingCode': typeof ComplexSlugBookingConfirmedBookingCodeRoute
'/admin/complexes': typeof AppAuthenticatedAdminComplexesRoute
'/admin/plans': typeof AppAuthenticatedAdminPlansRoute
'/admin/users': typeof AppAuthenticatedAdminUsersRoute
'/admin': typeof AppAuthenticatedAdminIndexRoute
'/complex/$slug/edit': typeof AppAuthenticatedComplexSlugEditRoute '/complex/$slug/edit': typeof AppAuthenticatedComplexSlugEditRoute
} }
export interface FileRoutesById { export interface FileRoutesById {
@@ -152,9 +196,14 @@ export interface FileRoutesById {
'/$complexSlug/booking': typeof ComplexSlugBookingRouteWithChildren '/$complexSlug/booking': typeof ComplexSlugBookingRouteWithChildren
'/_app/profile': typeof AppProfileRoute '/_app/profile': typeof AppProfileRoute
'/onboard/setup': typeof OnboardSetupRoute '/onboard/setup': typeof OnboardSetupRoute
'/_app/_authenticated/admin': typeof AppAuthenticatedAdminRouteRouteWithChildren
'/$complexSlug/booking/': typeof ComplexSlugBookingIndexRoute '/$complexSlug/booking/': typeof ComplexSlugBookingIndexRoute
'/_app/_authenticated/': typeof AppAuthenticatedIndexRoute '/_app/_authenticated/': typeof AppAuthenticatedIndexRoute
'/$complexSlug/booking/confirmed/$bookingCode': typeof ComplexSlugBookingConfirmedBookingCodeRoute '/$complexSlug/booking/confirmed/$bookingCode': typeof ComplexSlugBookingConfirmedBookingCodeRoute
'/_app/_authenticated/admin/complexes': typeof AppAuthenticatedAdminComplexesRoute
'/_app/_authenticated/admin/plans': typeof AppAuthenticatedAdminPlansRoute
'/_app/_authenticated/admin/users': typeof AppAuthenticatedAdminUsersRoute
'/_app/_authenticated/admin/': typeof AppAuthenticatedAdminIndexRoute
'/_app/_authenticated/complex/$slug/edit': typeof AppAuthenticatedComplexSlugEditRoute '/_app/_authenticated/complex/$slug/edit': typeof AppAuthenticatedComplexSlugEditRoute
} }
export interface FileRouteTypes { export interface FileRouteTypes {
@@ -171,8 +220,13 @@ export interface FileRouteTypes {
| '/$complexSlug/booking' | '/$complexSlug/booking'
| '/profile' | '/profile'
| '/onboard/setup' | '/onboard/setup'
| '/admin'
| '/$complexSlug/booking/' | '/$complexSlug/booking/'
| '/$complexSlug/booking/confirmed/$bookingCode' | '/$complexSlug/booking/confirmed/$bookingCode'
| '/admin/complexes'
| '/admin/plans'
| '/admin/users'
| '/admin/'
| '/complex/$slug/edit' | '/complex/$slug/edit'
fileRoutesByTo: FileRoutesByTo fileRoutesByTo: FileRoutesByTo
to: to:
@@ -188,6 +242,10 @@ export interface FileRouteTypes {
| '/onboard/setup' | '/onboard/setup'
| '/$complexSlug/booking' | '/$complexSlug/booking'
| '/$complexSlug/booking/confirmed/$bookingCode' | '/$complexSlug/booking/confirmed/$bookingCode'
| '/admin/complexes'
| '/admin/plans'
| '/admin/users'
| '/admin'
| '/complex/$slug/edit' | '/complex/$slug/edit'
id: id:
| '__root__' | '__root__'
@@ -203,9 +261,14 @@ export interface FileRouteTypes {
| '/$complexSlug/booking' | '/$complexSlug/booking'
| '/_app/profile' | '/_app/profile'
| '/onboard/setup' | '/onboard/setup'
| '/_app/_authenticated/admin'
| '/$complexSlug/booking/' | '/$complexSlug/booking/'
| '/_app/_authenticated/' | '/_app/_authenticated/'
| '/$complexSlug/booking/confirmed/$bookingCode' | '/$complexSlug/booking/confirmed/$bookingCode'
| '/_app/_authenticated/admin/complexes'
| '/_app/_authenticated/admin/plans'
| '/_app/_authenticated/admin/users'
| '/_app/_authenticated/admin/'
| '/_app/_authenticated/complex/$slug/edit' | '/_app/_authenticated/complex/$slug/edit'
fileRoutesById: FileRoutesById fileRoutesById: FileRoutesById
} }
@@ -321,6 +384,41 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof ComplexSlugBookingIndexRouteImport preLoaderRoute: typeof ComplexSlugBookingIndexRouteImport
parentRoute: typeof ComplexSlugBookingRoute parentRoute: typeof ComplexSlugBookingRoute
} }
'/_app/_authenticated/admin': {
id: '/_app/_authenticated/admin'
path: '/admin'
fullPath: '/admin'
preLoaderRoute: typeof AppAuthenticatedAdminRouteRouteImport
parentRoute: typeof AppAuthenticatedRouteRoute
}
'/_app/_authenticated/admin/': {
id: '/_app/_authenticated/admin/'
path: '/'
fullPath: '/admin/'
preLoaderRoute: typeof AppAuthenticatedAdminIndexRouteImport
parentRoute: typeof AppAuthenticatedAdminRouteRoute
}
'/_app/_authenticated/admin/users': {
id: '/_app/_authenticated/admin/users'
path: '/users'
fullPath: '/admin/users'
preLoaderRoute: typeof AppAuthenticatedAdminUsersRouteImport
parentRoute: typeof AppAuthenticatedAdminRouteRoute
}
'/_app/_authenticated/admin/plans': {
id: '/_app/_authenticated/admin/plans'
path: '/plans'
fullPath: '/admin/plans'
preLoaderRoute: typeof AppAuthenticatedAdminPlansRouteImport
parentRoute: typeof AppAuthenticatedAdminRouteRoute
}
'/_app/_authenticated/admin/complexes': {
id: '/_app/_authenticated/admin/complexes'
path: '/complexes'
fullPath: '/admin/complexes'
preLoaderRoute: typeof AppAuthenticatedAdminComplexesRouteImport
parentRoute: typeof AppAuthenticatedAdminRouteRoute
}
'/$complexSlug/booking/confirmed/$bookingCode': { '/$complexSlug/booking/confirmed/$bookingCode': {
id: '/$complexSlug/booking/confirmed/$bookingCode' id: '/$complexSlug/booking/confirmed/$bookingCode'
path: '/confirmed/$bookingCode' path: '/confirmed/$bookingCode'
@@ -338,12 +436,34 @@ declare module '@tanstack/react-router' {
} }
} }
interface AppAuthenticatedAdminRouteRouteChildren {
AppAuthenticatedAdminComplexesRoute: typeof AppAuthenticatedAdminComplexesRoute
AppAuthenticatedAdminPlansRoute: typeof AppAuthenticatedAdminPlansRoute
AppAuthenticatedAdminUsersRoute: typeof AppAuthenticatedAdminUsersRoute
AppAuthenticatedAdminIndexRoute: typeof AppAuthenticatedAdminIndexRoute
}
const AppAuthenticatedAdminRouteRouteChildren: AppAuthenticatedAdminRouteRouteChildren =
{
AppAuthenticatedAdminComplexesRoute: AppAuthenticatedAdminComplexesRoute,
AppAuthenticatedAdminPlansRoute: AppAuthenticatedAdminPlansRoute,
AppAuthenticatedAdminUsersRoute: AppAuthenticatedAdminUsersRoute,
AppAuthenticatedAdminIndexRoute: AppAuthenticatedAdminIndexRoute,
}
const AppAuthenticatedAdminRouteRouteWithChildren =
AppAuthenticatedAdminRouteRoute._addFileChildren(
AppAuthenticatedAdminRouteRouteChildren,
)
interface AppAuthenticatedRouteRouteChildren { interface AppAuthenticatedRouteRouteChildren {
AppAuthenticatedAdminRouteRoute: typeof AppAuthenticatedAdminRouteRouteWithChildren
AppAuthenticatedIndexRoute: typeof AppAuthenticatedIndexRoute AppAuthenticatedIndexRoute: typeof AppAuthenticatedIndexRoute
AppAuthenticatedComplexSlugEditRoute: typeof AppAuthenticatedComplexSlugEditRoute AppAuthenticatedComplexSlugEditRoute: typeof AppAuthenticatedComplexSlugEditRoute
} }
const AppAuthenticatedRouteRouteChildren: AppAuthenticatedRouteRouteChildren = { const AppAuthenticatedRouteRouteChildren: AppAuthenticatedRouteRouteChildren = {
AppAuthenticatedAdminRouteRoute: AppAuthenticatedAdminRouteRouteWithChildren,
AppAuthenticatedIndexRoute: AppAuthenticatedIndexRoute, AppAuthenticatedIndexRoute: AppAuthenticatedIndexRoute,
AppAuthenticatedComplexSlugEditRoute: AppAuthenticatedComplexSlugEditRoute, AppAuthenticatedComplexSlugEditRoute: AppAuthenticatedComplexSlugEditRoute,
} }

View File

@@ -0,0 +1,6 @@
import { ComplexesPage } from '@/features/admin/complexes-page';
import { createFileRoute } from '@tanstack/react-router';
export const Route = createFileRoute('/_app/_authenticated/admin/complexes')({
component: ComplexesPage,
});

View File

@@ -0,0 +1,6 @@
import { AdminPage } from '@/features/admin/admin-page';
import { createFileRoute } from '@tanstack/react-router';
export const Route = createFileRoute('/_app/_authenticated/admin/')({
component: AdminPage,
});

View File

@@ -0,0 +1,6 @@
import { PlansPage } from '@/features/admin/plans-page';
import { createFileRoute } from '@tanstack/react-router';
export const Route = createFileRoute('/_app/_authenticated/admin/plans')({
component: PlansPage,
});

View File

@@ -0,0 +1,16 @@
import { createFileRoute, redirect } from '@tanstack/react-router';
export const Route = createFileRoute('/_app/_authenticated/admin')({
beforeLoad: ({ context, location }) => {
if (!context.auth.isAuthenticated) {
throw redirect({
to: '/login',
search: { redirect: location.href },
});
}
if (context.auth.user?.role !== 'super_admin') {
throw redirect({ to: '/' });
}
},
});

View File

@@ -0,0 +1,6 @@
import { UsersPage } from '@/features/admin/users-page';
import { createFileRoute } from '@tanstack/react-router';
export const Route = createFileRoute('/_app/_authenticated/admin/users')({
component: UsersPage,
});

View File

@@ -1,2 +1,3 @@
[test] [test]
exclude = ["**/dist/**", "**/node_modules/**"]
preload = ["./apps/backend/test/support/prisma.mock.ts"] preload = ["./apps/backend/test/support/prisma.mock.ts"]

View File

@@ -1,16 +1,18 @@
import { z } from 'zod' import { z } from 'zod';
const TIME_REGEX = /^([01]\d|2[0-3]):([0-5]\d)$/ const TIME_REGEX = /^([01]\d|2[0-3]):([0-5]\d)$/;
const ISO_DATE_REGEX = /^\d{4}-\d{2}-\d{2}$/ const ISO_DATE_REGEX = /^\d{4}-\d{2}-\d{2}$/;
const BOOKING_CODE_REGEX = /^[A-Z0-9]{6,8}$/ const BOOKING_CODE_REGEX = /^[A-Z0-9]{6,8}$/;
export const bookingStatusSchema = z.enum(['CONFIRMED', 'CANCELLED', 'COMPLETED', 'NOSHOW']) export const bookingStatusSchema = z.enum(['CONFIRMED', 'CANCELLED', 'COMPLETED', 'NOSHOW']);
export const recurringGroupStatusSchema = z.enum(['ACTIVE', 'CANCELLED']);
export const adminBookingSportSchema = z.object({ export const adminBookingSportSchema = z.object({
id: z.uuid(), id: z.uuid(),
name: z.string(), name: z.string(),
slug: z.string(), slug: z.string(),
}) });
export const adminBookingSchema = z.object({ export const adminBookingSchema = z.object({
id: z.uuid(), id: z.uuid(),
@@ -28,17 +30,18 @@ export const adminBookingSchema = z.object({
customerEmail: z.string(), customerEmail: z.string(),
price: z.number().nonnegative(), price: z.number().nonnegative(),
status: bookingStatusSchema, status: bookingStatusSchema,
recurringGroupId: z.string().uuid().nullable().optional(),
createdAt: z.string().datetime(), createdAt: z.string().datetime(),
updatedAt: z.string().datetime(), updatedAt: z.string().datetime(),
}) });
export const listAdminBookingsQuerySchema = z.object({ export const listAdminBookingsQuerySchema = z.object({
fromDate: z.string().regex(ISO_DATE_REGEX, 'La fecha debe tener formato YYYY-MM-DD.'), fromDate: z.string().regex(ISO_DATE_REGEX, 'La fecha debe tener formato YYYY-MM-DD.'),
}) });
export const listAdminBookingsResponseSchema = z.object({ export const listAdminBookingsResponseSchema = z.object({
bookings: z.array(adminBookingSchema), bookings: z.array(adminBookingSchema),
}) });
export const createAdminBookingSchema = z.object({ export const createAdminBookingSchema = z.object({
date: z.string().regex(ISO_DATE_REGEX, 'La fecha debe tener formato YYYY-MM-DD.'), date: z.string().regex(ISO_DATE_REGEX, 'La fecha debe tener formato YYYY-MM-DD.'),
@@ -54,20 +57,73 @@ export const createAdminBookingSchema = z.object({
.trim() .trim()
.min(6, 'El telefono debe tener al menos 6 caracteres.') .min(6, 'El telefono debe tener al menos 6 caracteres.')
.max(30, 'El telefono no puede superar los 30 caracteres.'), .max(30, 'El telefono no puede superar los 30 caracteres.'),
customerEmail: z customerEmail: z.string().email('El email ingresado no es valido.').or(z.literal('')),
.string() });
.email('El email ingresado no es valido.')
.or(z.literal('')),
})
export const updateAdminBookingStatusSchema = z.object({ export const updateAdminBookingStatusSchema = z.object({
status: z.enum(['CANCELLED', 'COMPLETED', 'NOSHOW']), status: z.enum(['CANCELLED', 'COMPLETED', 'NOSHOW']),
}) });
export type BookingStatus = z.infer<typeof bookingStatusSchema> export const createRecurringBookingSchema = createAdminBookingSchema.extend({
export type AdminBookingSport = z.infer<typeof adminBookingSportSchema> isRecurring: z.literal(true),
export type AdminBooking = z.infer<typeof adminBookingSchema> recurringEndDate: z
export type ListAdminBookingsQuery = z.infer<typeof listAdminBookingsQuerySchema> .string()
export type ListAdminBookingsResponse = z.infer<typeof listAdminBookingsResponseSchema> .regex(ISO_DATE_REGEX, 'La fecha debe tener formato YYYY-MM-DD.')
export type CreateAdminBookingInput = z.infer<typeof createAdminBookingSchema> .optional(),
export type UpdateAdminBookingStatusInput = z.infer<typeof updateAdminBookingStatusSchema> });
export const recurringBookingGroupSchema = z.object({
id: z.uuid(),
complexId: z.uuid(),
courtId: z.uuid(),
startTime: z.string().regex(TIME_REGEX),
endTime: z.string().regex(TIME_REGEX),
dayOfWeek: z.enum(['MONDAY', 'TUESDAY', 'WEDNESDAY', 'THURSDAY', 'FRIDAY', 'SATURDAY', 'SUNDAY']),
startDate: z.string().regex(ISO_DATE_REGEX),
endDate: z.string().regex(ISO_DATE_REGEX).nullable(),
status: recurringGroupStatusSchema,
customerName: z.string(),
customerPhone: z.string(),
customerEmail: z.string(),
bookings: z.array(adminBookingSchema),
createdAt: z.string().datetime(),
updatedAt: z.string().datetime(),
});
export const updateRecurringGroupSchema = z.object({
courtId: z.uuid('El courtId debe ser un UUID valido.').optional(),
dayOfWeek: z
.enum(['MONDAY', 'TUESDAY', 'WEDNESDAY', 'THURSDAY', 'FRIDAY', 'SATURDAY', 'SUNDAY'])
.optional(),
startTime: z.string().regex(TIME_REGEX, 'La hora debe tener formato HH:mm.').optional(),
customerName: z
.string()
.trim()
.min(2, 'El nombre debe tener al menos 2 caracteres.')
.max(120, 'El nombre no puede superar los 120 caracteres.')
.optional(),
customerPhone: z
.string()
.trim()
.min(6, 'El telefono debe tener al menos 6 caracteres.')
.max(30, 'El telefono no puede superar los 30 caracteres.')
.optional(),
customerEmail: z.string().email('El email ingresado no es valido.').or(z.literal('')).optional(),
});
export const listRecurringGroupsResponseSchema = z.object({
groups: z.array(recurringBookingGroupSchema),
});
export type BookingStatus = z.infer<typeof bookingStatusSchema>;
export type RecurringGroupStatus = z.infer<typeof recurringGroupStatusSchema>;
export type AdminBookingSport = z.infer<typeof adminBookingSportSchema>;
export type AdminBooking = z.infer<typeof adminBookingSchema>;
export type ListAdminBookingsQuery = z.infer<typeof listAdminBookingsQuerySchema>;
export type ListAdminBookingsResponse = z.infer<typeof listAdminBookingsResponseSchema>;
export type CreateAdminBookingInput = z.infer<typeof createAdminBookingSchema>;
export type CreateRecurringBookingInput = z.infer<typeof createRecurringBookingSchema>;
export type RecurringBookingGroup = z.infer<typeof recurringBookingGroupSchema>;
export type UpdateAdminBookingStatusInput = z.infer<typeof updateAdminBookingStatusSchema>;
export type UpdateRecurringGroupInput = z.infer<typeof updateRecurringGroupSchema>;
export type ListRecurringGroupsResponse = z.infer<typeof listRecurringGroupsResponseSchema>;

View File

@@ -0,0 +1,116 @@
import { z } from 'zod';
export const adminPaymentStatusSchema = z.enum(['active', 'no_plan', 'expired']);
export const adminComplexListItemSchema = z.object({
id: z.string(),
complexName: z.string(),
complexSlug: z.string(),
city: z.string().nullable(),
planCode: z.string().nullable(),
planName: z.string().nullable(),
userCount: z.number().int().nonnegative(),
courtCount: z.number().int().nonnegative(),
avgBookingsPerDay: z.number().nonnegative(),
paymentStatus: adminPaymentStatusSchema,
});
export const adminComplexStatsSchema = z.object({
id: z.string(),
complexName: z.string(),
complexSlug: z.string(),
city: z.string().nullable(),
adminEmail: z.string(),
planCode: z.string().nullable(),
planName: z.string().nullable(),
userCount: z.number().int().nonnegative(),
courtCount: z.number().int().nonnegative(),
totalBookings: z.number().int().nonnegative(),
avgBookingsPerDay: z.number().nonnegative(),
bookingsByStatus: z.object({
confirmed: z.number().int().nonnegative(),
cancelled: z.number().int().nonnegative(),
completed: z.number().int().nonnegative(),
noshow: z.number().int().nonnegative(),
}),
paymentStatus: adminPaymentStatusSchema,
});
export const adminCreatePlanSchema = z.object({
code: z.string().trim().min(1).max(10),
name: z.string().trim().min(1).max(30),
price: z.number().nonnegative(),
rules: z.any(),
});
export const adminUpdatePlanSchema = z.object({
name: z.string().trim().min(1).max(30).optional(),
price: z.number().nonnegative().optional(),
rules: z.any().optional(),
});
export const adminUserSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string(),
role: z.string(),
banned: z.boolean(),
bannedAt: z.string().datetime().nullable(),
banReason: z.string().nullable(),
createdAt: z.string().datetime(),
complexCount: z.number().int().nonnegative(),
activeSessions: z.number().int().nonnegative(),
});
export const adminBlockUserSchema = z.object({
banReason: z.string().max(500).optional(),
});
export const adminGlobalStatsSchema = z.object({
totalComplexes: z.number().int().nonnegative(),
totalUsers: z.number().int().nonnegative(),
totalCourts: z.number().int().nonnegative(),
totalBookings: z.number().int().nonnegative(),
});
export const adminUserSessionSchema = z.object({
id: z.string(),
createdAt: z.string().datetime(),
expiresAt: z.string().datetime(),
ipAddress: z.string().nullable(),
userAgent: z.string().nullable(),
city: z.string().nullable(),
country: z.string().nullable(),
countryCode: z.string().nullable(),
});
export type AdminComplexListItem = z.infer<typeof adminComplexListItemSchema>;
export type AdminComplexStats = z.infer<typeof adminComplexStatsSchema>;
export type AdminCreatePlanInput = z.infer<typeof adminCreatePlanSchema>;
export type AdminUpdatePlanInput = z.infer<typeof adminUpdatePlanSchema>;
export type AdminUser = z.infer<typeof adminUserSchema>;
export type AdminBlockUserInput = z.infer<typeof adminBlockUserSchema>;
export type AdminGlobalStats = z.infer<typeof adminGlobalStatsSchema>;
export type AdminPaymentStatus = z.infer<typeof adminPaymentStatusSchema>;
export type AdminUserSession = z.infer<typeof adminUserSessionSchema>;
export const adminGeoCitySchema = z.object({
city: z.string(),
userCount: z.number().int().nonnegative(),
});
export const adminGeoCountrySchema = z.object({
country: z.string(),
countryCode: z.string(),
totalUsers: z.number().int().nonnegative(),
cities: z.array(adminGeoCitySchema),
});
export const adminGeoStatsSchema = z.object({
countries: z.array(adminGeoCountrySchema),
totalUniqueCountries: z.number().int().nonnegative(),
});
export type AdminGeoCity = z.infer<typeof adminGeoCitySchema>;
export type AdminGeoCountry = z.infer<typeof adminGeoCountrySchema>;
export type AdminGeoStats = z.infer<typeof adminGeoStatsSchema>;

View File

@@ -1,4 +1,5 @@
import { z } from 'zod' import { z } from 'zod';
import { planFeatureFlagsSchema } from './plan';
const dayOfWeekEnum = z.enum([ const dayOfWeekEnum = z.enum([
'MONDAY', 'MONDAY',
@@ -8,7 +9,7 @@ const dayOfWeekEnum = z.enum([
'FRIDAY', 'FRIDAY',
'SATURDAY', 'SATURDAY',
'SUNDAY', 'SUNDAY',
]) ]);
export const complexSchema = z.object({ export const complexSchema = z.object({
id: z.uuid(), id: z.uuid(),
@@ -22,7 +23,7 @@ export const complexSchema = z.object({
planCode: z.string().max(10).nullable(), planCode: z.string().max(10).nullable(),
createdAt: z.string().datetime(), createdAt: z.string().datetime(),
updatedAt: z.string().datetime(), updatedAt: z.string().datetime(),
}) });
export const createComplexSchema = z export const createComplexSchema = z
.object({ .object({
@@ -36,21 +37,13 @@ export const createComplexSchema = z
.trim() .trim()
.min(5, 'La direccion debe tener al menos 5 caracteres.') .min(5, 'La direccion debe tener al menos 5 caracteres.')
.max(200, 'La direccion no puede superar los 200 caracteres.'), .max(200, 'La direccion no puede superar los 200 caracteres.'),
city: z city: z.string().trim().max(100, 'La ciudad no puede superar los 100 caracteres.').optional(),
.string()
.trim()
.max(100, 'La ciudad no puede superar los 100 caracteres.')
.optional(),
state: z state: z
.string() .string()
.trim() .trim()
.max(100, 'La provincia/estado no puede superar los 100 caracteres.') .max(100, 'La provincia/estado no puede superar los 100 caracteres.')
.optional(), .optional(),
country: z country: z.string().trim().max(100, 'El país no puede superar los 100 caracteres.').optional(),
.string()
.trim()
.max(100, 'El país no puede superar los 100 caracteres.')
.optional(),
planCode: z planCode: z
.string() .string()
.trim() .trim()
@@ -70,31 +63,31 @@ export const createComplexSchema = z
code: z.ZodIssueCode.custom, code: z.ZodIssueCode.custom,
path: ['courtSportId'], path: ['courtSportId'],
message: 'Debes seleccionar un deporte.', message: 'Debes seleccionar un deporte.',
}) });
} }
if (!data.courtStartTime || !/^\d{2}:\d{2}$/.test(data.courtStartTime)) { if (!data.courtStartTime || !/^\d{2}:\d{2}$/.test(data.courtStartTime)) {
ctx.addIssue({ ctx.addIssue({
code: z.ZodIssueCode.custom, code: z.ZodIssueCode.custom,
path: ['courtStartTime'], path: ['courtStartTime'],
message: 'La hora de inicio debe tener formato HH:mm.', message: 'La hora de inicio debe tener formato HH:mm.',
}) });
} }
if (!data.courtEndTime || !/^\d{2}:\d{2}$/.test(data.courtEndTime)) { if (!data.courtEndTime || !/^\d{2}:\d{2}$/.test(data.courtEndTime)) {
ctx.addIssue({ ctx.addIssue({
code: z.ZodIssueCode.custom, code: z.ZodIssueCode.custom,
path: ['courtEndTime'], path: ['courtEndTime'],
message: 'La hora de fin debe tener formato HH:mm.', message: 'La hora de fin debe tener formato HH:mm.',
}) });
} }
if (!data.courtDaysOfWeek || data.courtDaysOfWeek.length === 0) { if (!data.courtDaysOfWeek || data.courtDaysOfWeek.length === 0) {
ctx.addIssue({ ctx.addIssue({
code: z.ZodIssueCode.custom, code: z.ZodIssueCode.custom,
path: ['courtDaysOfWeek'], path: ['courtDaysOfWeek'],
message: 'Debes seleccionar al menos un día.', message: 'Debes seleccionar al menos un día.',
}) });
} }
} }
}) });
export const updateComplexSchema = z export const updateComplexSchema = z
.object({ .object({
@@ -154,16 +147,16 @@ export const updateComplexSchema = z
payload.country !== undefined || payload.country !== undefined ||
payload.complexSlug || payload.complexSlug ||
payload.adminEmail || payload.adminEmail ||
payload.planCode !== undefined, payload.planCode !== undefined
), ),
{ {
message: 'Debes enviar al menos un campo para actualizar.', message: 'Debes enviar al menos un campo para actualizar.',
}, }
) );
export const complexUserRoleSchema = z.enum(['ADMIN', 'EMPLOYEE']) export const complexUserRoleSchema = z.enum(['ADMIN', 'EMPLOYEE']);
export type ComplexUserRole = z.infer<typeof complexUserRoleSchema> export type ComplexUserRole = z.infer<typeof complexUserRoleSchema>;
export const complexMemberSchema = z.object({ export const complexMemberSchema = z.object({
userId: z.string(), userId: z.string(),
@@ -172,13 +165,13 @@ export const complexMemberSchema = z.object({
avatarUrl: z.url(), avatarUrl: z.url(),
role: complexUserRoleSchema, role: complexUserRoleSchema,
createdAt: z.string().datetime(), createdAt: z.string().datetime(),
}) });
export type ComplexMember = z.infer<typeof complexMemberSchema> export type ComplexMember = z.infer<typeof complexMemberSchema>;
export const complexInvitationStatusSchema = z.enum(['PENDING', 'ACCEPTED', 'REVOKED', 'EXPIRED']) export const complexInvitationStatusSchema = z.enum(['PENDING', 'ACCEPTED', 'REVOKED', 'EXPIRED']);
export type ComplexInvitationStatus = z.infer<typeof complexInvitationStatusSchema> export type ComplexInvitationStatus = z.infer<typeof complexInvitationStatusSchema>;
export const complexInvitationSchema = z.object({ export const complexInvitationSchema = z.object({
id: z.uuid(), id: z.uuid(),
@@ -189,62 +182,63 @@ export const complexInvitationSchema = z.object({
expiresAt: z.string().datetime(), expiresAt: z.string().datetime(),
acceptedAt: z.string().datetime().nullable(), acceptedAt: z.string().datetime().nullable(),
revokedAt: z.string().datetime().nullable(), revokedAt: z.string().datetime().nullable(),
}) });
export type ComplexInvitation = z.infer<typeof complexInvitationSchema> export type ComplexInvitation = z.infer<typeof complexInvitationSchema>;
export const complexUsersOverviewSchema = z.object({ export const complexUsersOverviewSchema = z.object({
members: z.array(complexMemberSchema), members: z.array(complexMemberSchema),
invitations: z.array(complexInvitationSchema), invitations: z.array(complexInvitationSchema),
}) });
export type ComplexUsersOverview = z.infer<typeof complexUsersOverviewSchema> export type ComplexUsersOverview = z.infer<typeof complexUsersOverviewSchema>;
export const inviteComplexUserSchema = z.object({ export const inviteComplexUserSchema = z.object({
email: z.email('El email debe ser válido.'), email: z.email('El email debe ser válido.'),
}) });
export type InviteComplexUserInput = z.infer<typeof inviteComplexUserSchema> export type InviteComplexUserInput = z.infer<typeof inviteComplexUserSchema>;
export const acceptComplexInvitationsSchema = z.object({ export const acceptComplexInvitationsSchema = z.object({
inviteToken: z.string().min(1, 'El token de invitación no puede estar vacío.').optional(), inviteToken: z.string().min(1, 'El token de invitación no puede estar vacío.').optional(),
}) });
export type AcceptComplexInvitationsInput = z.infer<typeof acceptComplexInvitationsSchema> export type AcceptComplexInvitationsInput = z.infer<typeof acceptComplexInvitationsSchema>;
export const revokeComplexUserSchema = z.object({ export const revokeComplexUserSchema = z.object({
userId: z.string().min(1, 'El userId no puede estar vacío.'), userId: z.string().min(1, 'El userId no puede estar vacío.'),
}) });
export type RevokeComplexUserInput = z.infer<typeof revokeComplexUserSchema> export type RevokeComplexUserInput = z.infer<typeof revokeComplexUserSchema>;
export const selectComplexSchema = z.object({ export const selectComplexSchema = z.object({
complexId: z.string().uuid(), complexId: z.string().uuid(),
}) });
export type SelectComplexInput = z.infer<typeof selectComplexSchema> export type SelectComplexInput = z.infer<typeof selectComplexSchema>;
export const complexWithRoleSchema = complexSchema.merge( export const complexWithRoleSchema = complexSchema.merge(
z.object({ z.object({
role: complexUserRoleSchema, role: complexUserRoleSchema,
planFeatures: planFeatureFlagsSchema.nullable(),
}) })
) );
export type ComplexWithRole = z.infer<typeof complexWithRoleSchema> export type ComplexWithRole = z.infer<typeof complexWithRoleSchema>;
export type Complex = z.infer<typeof complexSchema> export type Complex = z.infer<typeof complexSchema>;
export type CreateComplexInput = z.infer<typeof createComplexSchema> export type CreateComplexInput = z.infer<typeof createComplexSchema>;
export type UpdateComplexInput = z.infer<typeof updateComplexSchema> export type UpdateComplexInput = z.infer<typeof updateComplexSchema>;
export type CreateComplexPayload = CreateComplexInput export type CreateComplexPayload = CreateComplexInput;
export type CreateComplexResponse = Complex export type CreateComplexResponse = Complex;
export type UpdateComplexPayload = UpdateComplexInput export type UpdateComplexPayload = UpdateComplexInput;
export type UpdateComplexResponse = Complex export type UpdateComplexResponse = Complex;
export type InviteComplexUserResponse = { export type InviteComplexUserResponse = {
invitation: ComplexInvitation invitation: ComplexInvitation;
} };
export type AcceptComplexInvitationsResponse = { export type AcceptComplexInvitationsResponse = {
acceptedCount: number acceptedCount: number;
} };
export type CancelComplexInvitationResponse = { export type CancelComplexInvitationResponse = {
ok: boolean ok: boolean;
} };

View File

@@ -1,65 +1,65 @@
import { z } from 'zod' import { z } from 'zod';
const TIME_REGEX = /^([01]\d|2[0-3]):([0-5]\d)$/ const TIME_REGEX = /^([01]\d|2[0-3]):([0-5]\d)$/;
function toMinutes(value: string): number { function toMinutes(value: string): number {
const [hours, minutes] = value.split(':').map((part) => Number(part)) const [hours, minutes] = value.split(':').map((part) => Number(part));
return hours * 60 + minutes return hours * 60 + minutes;
} }
function hasOverlaps( function hasOverlaps(
availability: Array<{ availability: Array<{
dayOfWeek: DayOfWeek dayOfWeek: DayOfWeek;
startTime: string startTime: string;
endTime: string endTime: string;
}>, }>
): boolean { ): boolean {
const grouped = new Map< const grouped = new Map<
DayOfWeek, DayOfWeek,
Array<{ Array<{
start: number start: number;
end: number end: number;
}> }>
>() >();
for (const range of availability) { for (const range of availability) {
const start = toMinutes(range.startTime) const start = toMinutes(range.startTime);
const end = toMinutes(range.endTime) const end = toMinutes(range.endTime);
const current = grouped.get(range.dayOfWeek) ?? [] const current = grouped.get(range.dayOfWeek) ?? [];
current.push({ start, end }) current.push({ start, end });
grouped.set(range.dayOfWeek, current) grouped.set(range.dayOfWeek, current);
} }
for (const ranges of grouped.values()) { for (const ranges of grouped.values()) {
ranges.sort((a, b) => a.start - b.start) ranges.sort((a, b) => a.start - b.start);
for (let index = 1; index < ranges.length; index += 1) { for (let index = 1; index < ranges.length; index += 1) {
const previous = ranges[index - 1] const previous = ranges[index - 1];
const current = ranges[index] const current = ranges[index];
if (previous.end > current.start) { if (previous.end > current.start) {
return true return true;
} }
} }
} }
return false return false;
} }
function refineAvailability( function refineAvailability(
availability: Array<{ availability: Array<{
dayOfWeek: DayOfWeek dayOfWeek: DayOfWeek;
startTime: string startTime: string;
endTime: string endTime: string;
}>, }>,
context: z.RefinementCtx, context: z.RefinementCtx
) { ) {
for (const range of availability) { for (const range of availability) {
if (toMinutes(range.startTime) >= toMinutes(range.endTime)) { if (toMinutes(range.startTime) >= toMinutes(range.endTime)) {
context.addIssue({ context.addIssue({
code: z.ZodIssueCode.custom, code: z.ZodIssueCode.custom,
message: `El rango ${range.startTime}-${range.endTime} es invalido. La hora de inicio debe ser menor a la de fin.`, message: `El rango ${range.startTime}-${range.endTime} es invalido. La hora de inicio debe ser menor a la de fin.`,
}) });
} }
} }
@@ -67,7 +67,7 @@ function refineAvailability(
context.addIssue({ context.addIssue({
code: z.ZodIssueCode.custom, code: z.ZodIssueCode.custom,
message: 'Hay rangos horarios superpuestos para el mismo dia.', message: 'Hay rangos horarios superpuestos para el mismo dia.',
}) });
} }
} }
@@ -79,22 +79,22 @@ export const dayOfWeekSchema = z.enum([
'FRIDAY', 'FRIDAY',
'SATURDAY', 'SATURDAY',
'SUNDAY', 'SUNDAY',
]) ]);
export type DayOfWeek = z.infer<typeof dayOfWeekSchema> export type DayOfWeek = z.infer<typeof dayOfWeekSchema>;
export const courtAvailabilitySchema = z.object({ export const courtAvailabilitySchema = z.object({
id: z.uuid(), id: z.uuid(),
dayOfWeek: dayOfWeekSchema, dayOfWeek: dayOfWeekSchema,
startTime: z.string().regex(TIME_REGEX, 'La hora debe tener formato HH:mm.'), startTime: z.string().regex(TIME_REGEX, 'La hora debe tener formato HH:mm.'),
endTime: z.string().regex(TIME_REGEX, 'La hora debe tener formato HH:mm.'), endTime: z.string().regex(TIME_REGEX, 'La hora debe tener formato HH:mm.'),
}) });
export const courtAvailabilityInputSchema = z.object({ export const courtAvailabilityInputSchema = z.object({
dayOfWeek: dayOfWeekSchema, dayOfWeek: dayOfWeekSchema,
startTime: z.string().regex(TIME_REGEX, 'La hora debe tener formato HH:mm.'), startTime: z.string().regex(TIME_REGEX, 'La hora debe tener formato HH:mm.'),
endTime: z.string().regex(TIME_REGEX, 'La hora debe tener formato HH:mm.'), endTime: z.string().regex(TIME_REGEX, 'La hora debe tener formato HH:mm.'),
}) });
export const courtPriceRuleSchema = z.object({ export const courtPriceRuleSchema = z.object({
id: z.uuid(), id: z.uuid(),
@@ -103,13 +103,13 @@ export const courtPriceRuleSchema = z.object({
endTime: z.string().nullable(), endTime: z.string().nullable(),
price: z.number().nonnegative(), price: z.number().nonnegative(),
isActive: z.boolean(), isActive: z.boolean(),
}) });
export const courtSportSchema = z.object({ export const courtSportSchema = z.object({
id: z.uuid(), id: z.uuid(),
name: z.string(), name: z.string(),
slug: z.string(), slug: z.string(),
}) });
export const courtSchema = z.object({ export const courtSchema = z.object({
id: z.uuid(), id: z.uuid(),
@@ -124,7 +124,7 @@ export const courtSchema = z.object({
hasCustomPricing: z.boolean(), hasCustomPricing: z.boolean(),
createdAt: z.string().datetime(), createdAt: z.string().datetime(),
updatedAt: z.string().datetime(), updatedAt: z.string().datetime(),
}) });
export const createCourtSchema = z export const createCourtSchema = z
.object({ .object({
@@ -139,9 +139,11 @@ export const createCourtSchema = z
.min(15, 'La duracion minima por turno es 15 minutos.') .min(15, 'La duracion minima por turno es 15 minutos.')
.max(480, 'La duracion maxima por turno es 480 minutos.'), .max(480, 'La duracion maxima por turno es 480 minutos.'),
basePrice: z.number().nonnegative('El precio base no puede ser negativo.'), basePrice: z.number().nonnegative('El precio base no puede ser negativo.'),
availability: z.array(courtAvailabilityInputSchema).min(1, 'Debes definir al menos un rango horario.'), availability: z
.array(courtAvailabilityInputSchema)
.min(1, 'Debes definir al menos un rango horario.'),
}) })
.superRefine((payload, context) => refineAvailability(payload.availability, context)) .superRefine((payload, context) => refineAvailability(payload.availability, context));
export const updateCourtSchema = z export const updateCourtSchema = z
.object({ .object({
@@ -158,7 +160,10 @@ export const updateCourtSchema = z
.max(480, 'La duracion maxima por turno es 480 minutos.') .max(480, 'La duracion maxima por turno es 480 minutos.')
.optional(), .optional(),
basePrice: z.number().nonnegative('El precio base no puede ser negativo.').optional(), basePrice: z.number().nonnegative('El precio base no puede ser negativo.').optional(),
availability: z.array(courtAvailabilityInputSchema).min(1, 'Debes definir al menos un rango horario.').optional(), availability: z
.array(courtAvailabilityInputSchema)
.min(1, 'Debes definir al menos un rango horario.')
.optional(),
}) })
.superRefine((payload, context) => { .superRefine((payload, context) => {
if ( if (
@@ -171,17 +176,17 @@ export const updateCourtSchema = z
context.addIssue({ context.addIssue({
code: z.ZodIssueCode.custom, code: z.ZodIssueCode.custom,
message: 'Debes enviar al menos un campo para actualizar.', message: 'Debes enviar al menos un campo para actualizar.',
}) });
} }
if (payload.availability) { if (payload.availability) {
refineAvailability(payload.availability, context) refineAvailability(payload.availability, context);
} }
}) });
export type Court = z.infer<typeof courtSchema> export type Court = z.infer<typeof courtSchema>;
export type CourtAvailability = z.infer<typeof courtAvailabilitySchema> export type CourtAvailability = z.infer<typeof courtAvailabilitySchema>;
export type CourtAvailabilityInput = z.infer<typeof courtAvailabilityInputSchema> export type CourtAvailabilityInput = z.infer<typeof courtAvailabilityInputSchema>;
export type CourtPriceRule = z.infer<typeof courtPriceRuleSchema> export type CourtPriceRule = z.infer<typeof courtPriceRuleSchema>;
export type CreateCourtInput = z.infer<typeof createCourtSchema> export type CreateCourtInput = z.infer<typeof createCourtSchema>;
export type UpdateCourtInput = z.infer<typeof updateCourtSchema> export type UpdateCourtInput = z.infer<typeof updateCourtSchema>;

View File

@@ -6,7 +6,7 @@ export {
planSummarySchema, planSummarySchema,
planWithFeaturesSchema, planWithFeaturesSchema,
planLimitsSchema, planLimitsSchema,
} from './plan' } from './plan';
export type { export type {
Plan, Plan,
PlanFeatureFlags, PlanFeatureFlags,
@@ -15,7 +15,7 @@ export type {
PlanSummary, PlanSummary,
PlanWithFeatures, PlanWithFeatures,
PlanLimits, PlanLimits,
} from './plan' } from './plan';
export { export {
acceptComplexInvitationsSchema, acceptComplexInvitationsSchema,
complexInvitationSchema, complexInvitationSchema,
@@ -30,7 +30,7 @@ export {
revokeComplexUserSchema, revokeComplexUserSchema,
selectComplexSchema, selectComplexSchema,
updateComplexSchema, updateComplexSchema,
} from './complex' } from './complex';
export type { export type {
AcceptComplexInvitationsInput, AcceptComplexInvitationsInput,
AcceptComplexInvitationsResponse, AcceptComplexInvitationsResponse,
@@ -52,7 +52,7 @@ export type {
UpdateComplexPayload, UpdateComplexPayload,
UpdateComplexResponse, UpdateComplexResponse,
RevokeComplexUserInput, RevokeComplexUserInput,
} from './complex' } from './complex';
export { export {
dayOfWeekSchema, dayOfWeekSchema,
courtAvailabilityInputSchema, courtAvailabilityInputSchema,
@@ -61,7 +61,7 @@ export {
courtSchema, courtSchema,
createCourtSchema, createCourtSchema,
updateCourtSchema, updateCourtSchema,
} from './court' } from './court';
export type { export type {
DayOfWeek, DayOfWeek,
Court, Court,
@@ -70,25 +70,35 @@ export type {
CourtPriceRule, CourtPriceRule,
CreateCourtInput, CreateCourtInput,
UpdateCourtInput, UpdateCourtInput,
} from './court' } from './court';
export { export {
adminBookingSchema, adminBookingSchema,
adminBookingSportSchema, adminBookingSportSchema,
bookingStatusSchema, bookingStatusSchema,
createAdminBookingSchema, createAdminBookingSchema,
createRecurringBookingSchema,
listAdminBookingsQuerySchema, listAdminBookingsQuerySchema,
listAdminBookingsResponseSchema, listAdminBookingsResponseSchema,
listRecurringGroupsResponseSchema,
recurringBookingGroupSchema,
recurringGroupStatusSchema,
updateAdminBookingStatusSchema, updateAdminBookingStatusSchema,
} from './admin-booking' updateRecurringGroupSchema,
} from './admin-booking';
export type { export type {
AdminBooking, AdminBooking,
AdminBookingSport, AdminBookingSport,
BookingStatus, BookingStatus,
CreateAdminBookingInput, CreateAdminBookingInput,
CreateRecurringBookingInput,
ListAdminBookingsQuery, ListAdminBookingsQuery,
ListAdminBookingsResponse, ListAdminBookingsResponse,
ListRecurringGroupsResponse,
RecurringBookingGroup,
RecurringGroupStatus,
UpdateAdminBookingStatusInput, UpdateAdminBookingStatusInput,
} from './admin-booking' UpdateRecurringGroupInput,
} from './admin-booking';
export { export {
createPublicBookingSchema, createPublicBookingSchema,
publicAvailabilityCourtSchema, publicAvailabilityCourtSchema,
@@ -99,7 +109,7 @@ export {
publicBookingSlotSchema, publicBookingSlotSchema,
publicBookingSportSchema, publicBookingSportSchema,
cancelPublicBookingSchema, cancelPublicBookingSchema,
} from './public-booking' } from './public-booking';
export type { export type {
CreatePublicBookingInput, CreatePublicBookingInput,
CancelPublicBookingInput, CancelPublicBookingInput,
@@ -110,15 +120,15 @@ export type {
PublicBookingConfirmation, PublicBookingConfirmation,
PublicBookingSlot, PublicBookingSlot,
PublicBookingSport, PublicBookingSport,
} from './public-booking' } from './public-booking';
export { sportSchema, createSportSchema, updateSportSchema } from './sport' export { sportSchema, createSportSchema, updateSportSchema } from './sport';
export type { Sport, CreateSportInput, UpdateSportInput } from './sport' export type { Sport, CreateSportInput, UpdateSportInput } from './sport';
export { export {
userProfileSchema, userProfileSchema,
passwordResetStartSchema, passwordResetStartSchema,
passwordResetVerifySchema, passwordResetVerifySchema,
passwordResetResendSchema, passwordResetResendSchema,
} from './user' } from './user';
export type { export type {
UserProfile, UserProfile,
UserProfileResponse, UserProfileResponse,
@@ -128,4 +138,32 @@ export type {
PasswordResetStartResponse, PasswordResetStartResponse,
PasswordResetVerifyResponse, PasswordResetVerifyResponse,
PasswordResetResendResponse, PasswordResetResendResponse,
} from './user' } from './user';
export {
adminComplexListItemSchema,
adminComplexStatsSchema,
adminCreatePlanSchema,
adminUpdatePlanSchema,
adminUserSchema,
adminBlockUserSchema,
adminGlobalStatsSchema,
adminPaymentStatusSchema,
adminUserSessionSchema,
adminGeoCitySchema,
adminGeoCountrySchema,
adminGeoStatsSchema,
} from './admin';
export type {
AdminComplexListItem,
AdminComplexStats,
AdminCreatePlanInput,
AdminUpdatePlanInput,
AdminUser,
AdminBlockUserInput,
AdminGlobalStats,
AdminPaymentStatus,
AdminUserSession,
AdminGeoCity,
AdminGeoCountry,
AdminGeoStats,
} from './admin';

View File

@@ -1,14 +1,15 @@
import { z } from 'zod' import { z } from 'zod';
const nonNegativeInt = z.int().nonnegative() const nonNegativeInt = z.int().nonnegative();
const positiveInt = z.int().positive() const positiveInt = z.int().positive();
export const planFeatureFlagsSchema = z.object({ export const planFeatureFlagsSchema = z.object({
onlinePayments: z.boolean().default(false), onlinePayments: z.boolean().default(false),
publicBookingPage: z.boolean().default(false), publicBookingPage: z.boolean().default(false),
advancedReports: z.boolean().default(false), advancedReports: z.boolean().default(false),
whatsappReminders: z.boolean().default(false), whatsappReminders: z.boolean().default(false),
}) fixedSlots: z.boolean().default(false),
});
export const planRulesV1Schema = z.object({ export const planRulesV1Schema = z.object({
version: z.literal('v1'), version: z.literal('v1'),
@@ -17,6 +18,7 @@ export const planRulesV1Schema = z.object({
maxBookingsPerDay: positiveInt, maxBookingsPerDay: positiveInt,
maxActiveUsers: positiveInt.optional(), maxActiveUsers: positiveInt.optional(),
maxConcurrentBookingsPerSlot: positiveInt.optional(), maxConcurrentBookingsPerSlot: positiveInt.optional(),
maxSports: positiveInt.optional(),
}), }),
policies: z policies: z
.object({ .object({
@@ -30,10 +32,28 @@ export const planRulesV1Schema = z.object({
publicBookingPage: false, publicBookingPage: false,
advancedReports: false, advancedReports: false,
whatsappReminders: false, whatsappReminders: false,
fixedSlots: false,
}), }),
}) });
export const planRulesSchema = z.discriminatedUnion('version', [planRulesV1Schema]) export const planPriceOverrideSchema = z.object({
amount: z.number().nonnegative(),
currency: z.string().length(3),
});
export const planRulesV2Schema = planRulesV1Schema.extend({
version: z.literal('v2'),
pricing: z
.object({
overrides: z.record(z.string(), planPriceOverrideSchema),
})
.optional(),
});
export const planRulesSchema = z.discriminatedUnion('version', [
planRulesV1Schema,
planRulesV2Schema,
]);
export const planSchema = z.object({ export const planSchema = z.object({
code: z.string().trim().min(1).max(10), code: z.string().trim().min(1).max(10),
@@ -41,33 +61,36 @@ export const planSchema = z.object({
price: z.number().nonnegative(), price: z.number().nonnegative(),
lastUpdatedAt: z.string().datetime(), lastUpdatedAt: z.string().datetime(),
rules: planRulesSchema, rules: planRulesSchema,
}) });
export const planSummarySchema = z.object({ export const planSummarySchema = z.object({
code: z.string().trim().min(1).max(10), code: z.string().trim().min(1).max(10),
name: z.string().trim().min(1).max(30), name: z.string().trim().min(1).max(30),
price: z.number().nonnegative(), price: z.number().nonnegative(),
}) });
export const planLimitsSchema = z.object({ export const planLimitsSchema = z.object({
maxCourts: positiveInt, maxCourts: positiveInt,
maxBookingsPerDay: positiveInt, maxBookingsPerDay: positiveInt,
maxActiveUsers: positiveInt.optional(), maxActiveUsers: positiveInt.optional(),
maxConcurrentBookingsPerSlot: positiveInt.optional(), maxConcurrentBookingsPerSlot: positiveInt.optional(),
}) maxSports: positiveInt.optional(),
});
export const planWithFeaturesSchema = z.object({ export const planWithFeaturesSchema = z.object({
code: z.string().trim().min(1).max(10), code: z.string().trim().min(1).max(10),
name: z.string().trim().min(1).max(30), name: z.string().trim().min(1).max(30),
price: z.number().nonnegative(), price: z.number().nonnegative(),
currency: z.string().length(3),
features: planFeatureFlagsSchema, features: planFeatureFlagsSchema,
limits: planLimitsSchema, limits: planLimitsSchema,
}) });
export type PlanFeatureFlags = z.infer<typeof planFeatureFlagsSchema> export type PlanFeatureFlags = z.infer<typeof planFeatureFlagsSchema>;
export type PlanRulesV1 = z.infer<typeof planRulesV1Schema> export type PlanRulesV1 = z.infer<typeof planRulesV1Schema>;
export type PlanRules = z.infer<typeof planRulesSchema> export type PlanRulesV2 = z.infer<typeof planRulesV2Schema>;
export type Plan = z.infer<typeof planSchema> export type PlanRules = z.infer<typeof planRulesSchema>;
export type PlanSummary = z.infer<typeof planSummarySchema> export type Plan = z.infer<typeof planSchema>;
export type PlanWithFeatures = z.infer<typeof planWithFeaturesSchema> export type PlanSummary = z.infer<typeof planSummarySchema>;
export type PlanLimits = z.infer<typeof planLimitsSchema> export type PlanWithFeatures = z.infer<typeof planWithFeaturesSchema>;
export type PlanLimits = z.infer<typeof planLimitsSchema>;

View File

@@ -1,26 +1,26 @@
import { z } from 'zod' import { z } from 'zod';
import { dayOfWeekSchema } from './court' import { dayOfWeekSchema } from './court';
const TIME_REGEX = /^([01]\d|2[0-3]):([0-5]\d)$/ const TIME_REGEX = /^([01]\d|2[0-3]):([0-5]\d)$/;
const ISO_DATE_REGEX = /^\d{4}-\d{2}-\d{2}$/ const ISO_DATE_REGEX = /^\d{4}-\d{2}-\d{2}$/;
const BOOKING_CODE_REGEX = /^[A-Z0-9]{6,8}$/ const BOOKING_CODE_REGEX = /^[A-Z0-9]{6,8}$/;
export const publicAvailabilityQuerySchema = z.object({ export const publicAvailabilityQuerySchema = z.object({
date: z.string().regex(ISO_DATE_REGEX, 'La fecha debe tener formato YYYY-MM-DD.'), date: z.string().regex(ISO_DATE_REGEX, 'La fecha debe tener formato YYYY-MM-DD.'),
sportId: z.uuid('El sportId debe ser un UUID valido.').optional(), sportId: z.uuid('El sportId debe ser un UUID valido.').optional(),
}) });
export const publicBookingSportSchema = z.object({ export const publicBookingSportSchema = z.object({
id: z.uuid(), id: z.uuid(),
name: z.string(), name: z.string(),
slug: z.string(), slug: z.string(),
}) });
export const publicBookingSlotSchema = z.object({ export const publicBookingSlotSchema = z.object({
startTime: z.string().regex(TIME_REGEX), startTime: z.string().regex(TIME_REGEX),
endTime: z.string().regex(TIME_REGEX), endTime: z.string().regex(TIME_REGEX),
price: z.number().nonnegative(), price: z.number().nonnegative(),
}) });
export const publicAvailabilityCourtSchema = z.object({ export const publicAvailabilityCourtSchema = z.object({
courtId: z.uuid(), courtId: z.uuid(),
@@ -29,7 +29,7 @@ export const publicAvailabilityCourtSchema = z.object({
slotDurationMinutes: z.int().positive(), slotDurationMinutes: z.int().positive(),
availabilityDay: dayOfWeekSchema, availabilityDay: dayOfWeekSchema,
availableSlots: z.array(publicBookingSlotSchema), availableSlots: z.array(publicBookingSlotSchema),
}) });
export const publicAvailabilityResponseSchema = z.object({ export const publicAvailabilityResponseSchema = z.object({
complexId: z.uuid(), complexId: z.uuid(),
@@ -40,7 +40,7 @@ export const publicAvailabilityResponseSchema = z.object({
sportSelectionRequired: z.boolean(), sportSelectionRequired: z.boolean(),
sports: z.array(publicBookingSportSchema), sports: z.array(publicBookingSportSchema),
courts: z.array(publicAvailabilityCourtSchema), courts: z.array(publicAvailabilityCourtSchema),
}) });
export const createPublicBookingSchema = z.object({ export const createPublicBookingSchema = z.object({
date: z.string().regex(ISO_DATE_REGEX, 'La fecha debe tener formato YYYY-MM-DD.'), date: z.string().regex(ISO_DATE_REGEX, 'La fecha debe tener formato YYYY-MM-DD.'),
@@ -57,10 +57,8 @@ export const createPublicBookingSchema = z.object({
.trim() .trim()
.min(6, 'El telefono debe tener al menos 6 caracteres.') .min(6, 'El telefono debe tener al menos 6 caracteres.')
.max(30, 'El telefono no puede superar los 30 caracteres.'), .max(30, 'El telefono no puede superar los 30 caracteres.'),
customerEmail: z customerEmail: z.string().email('El email ingresado no es valido.'),
.string() });
.email('El email ingresado no es valido.'),
})
export const cancelPublicBookingSchema = z.object({ export const cancelPublicBookingSchema = z.object({
bookingCode: z.string().regex(BOOKING_CODE_REGEX, 'El codigo de reserva no es valido.'), bookingCode: z.string().regex(BOOKING_CODE_REGEX, 'El codigo de reserva no es valido.'),
@@ -69,7 +67,7 @@ export const cancelPublicBookingSchema = z.object({
.trim() .trim()
.min(6, 'El telefono debe tener al menos 6 caracteres.') .min(6, 'El telefono debe tener al menos 6 caracteres.')
.max(30, 'El telefono no puede superar los 30 caracteres.'), .max(30, 'El telefono no puede superar los 30 caracteres.'),
}) });
export const publicBookingSchema = z.object({ export const publicBookingSchema = z.object({
id: z.uuid(), id: z.uuid(),
@@ -89,7 +87,7 @@ export const publicBookingSchema = z.object({
status: z.enum(['CONFIRMED', 'CANCELLED', 'COMPLETED']), status: z.enum(['CONFIRMED', 'CANCELLED', 'COMPLETED']),
price: z.number().nonnegative(), price: z.number().nonnegative(),
createdAt: z.string().datetime(), createdAt: z.string().datetime(),
}) });
export const publicBookingConfirmationSchema = z.object({ export const publicBookingConfirmationSchema = z.object({
bookingCode: z.string().regex(BOOKING_CODE_REGEX), bookingCode: z.string().regex(BOOKING_CODE_REGEX),
@@ -105,14 +103,14 @@ export const publicBookingConfirmationSchema = z.object({
status: z.enum(['CONFIRMED', 'CANCELLED', 'COMPLETED']), status: z.enum(['CONFIRMED', 'CANCELLED', 'COMPLETED']),
customerEmail: z.string(), customerEmail: z.string(),
createdAt: z.string().datetime(), createdAt: z.string().datetime(),
}) });
export type PublicAvailabilityQuery = z.infer<typeof publicAvailabilityQuerySchema> export type PublicAvailabilityQuery = z.infer<typeof publicAvailabilityQuerySchema>;
export type PublicBookingSport = z.infer<typeof publicBookingSportSchema> export type PublicBookingSport = z.infer<typeof publicBookingSportSchema>;
export type PublicBookingSlot = z.infer<typeof publicBookingSlotSchema> export type PublicBookingSlot = z.infer<typeof publicBookingSlotSchema>;
export type PublicAvailabilityCourt = z.infer<typeof publicAvailabilityCourtSchema> export type PublicAvailabilityCourt = z.infer<typeof publicAvailabilityCourtSchema>;
export type PublicAvailabilityResponse = z.infer<typeof publicAvailabilityResponseSchema> export type PublicAvailabilityResponse = z.infer<typeof publicAvailabilityResponseSchema>;
export type CreatePublicBookingInput = z.infer<typeof createPublicBookingSchema> export type CreatePublicBookingInput = z.infer<typeof createPublicBookingSchema>;
export type CancelPublicBookingInput = z.infer<typeof cancelPublicBookingSchema> export type CancelPublicBookingInput = z.infer<typeof cancelPublicBookingSchema>;
export type PublicBooking = z.infer<typeof publicBookingSchema> export type PublicBooking = z.infer<typeof publicBookingSchema>;
export type PublicBookingConfirmation = z.infer<typeof publicBookingConfirmationSchema> export type PublicBookingConfirmation = z.infer<typeof publicBookingConfirmationSchema>;

View File

@@ -1,4 +1,4 @@
import { z } from 'zod' import { z } from 'zod';
export const sportSchema = z.object({ export const sportSchema = z.object({
id: z.uuid(), id: z.uuid(),
@@ -7,7 +7,7 @@ export const sportSchema = z.object({
isActive: z.boolean(), isActive: z.boolean(),
createdAt: z.string().datetime(), createdAt: z.string().datetime(),
updatedAt: z.string().datetime(), updatedAt: z.string().datetime(),
}) });
export const createSportSchema = z.object({ export const createSportSchema = z.object({
name: z name: z
@@ -15,7 +15,7 @@ export const createSportSchema = z.object({
.trim() .trim()
.min(2, 'El deporte debe tener al menos 2 caracteres.') .min(2, 'El deporte debe tener al menos 2 caracteres.')
.max(80, 'El deporte no puede superar los 80 caracteres.'), .max(80, 'El deporte no puede superar los 80 caracteres.'),
}) });
export const updateSportSchema = z export const updateSportSchema = z
.object({ .object({
@@ -29,8 +29,8 @@ export const updateSportSchema = z
}) })
.refine((payload) => payload.name !== undefined || payload.isActive !== undefined, { .refine((payload) => payload.name !== undefined || payload.isActive !== undefined, {
message: 'Debes enviar al menos un campo para actualizar.', message: 'Debes enviar al menos un campo para actualizar.',
}) });
export type Sport = z.infer<typeof sportSchema> export type Sport = z.infer<typeof sportSchema>;
export type CreateSportInput = z.infer<typeof createSportSchema> export type CreateSportInput = z.infer<typeof createSportSchema>;
export type UpdateSportInput = z.infer<typeof updateSportSchema> export type UpdateSportInput = z.infer<typeof updateSportSchema>;

View File

@@ -1,4 +1,4 @@
import { z } from 'zod' import { z } from 'zod';
export const userProfileSchema = z.object({ export const userProfileSchema = z.object({
id: z.string(), id: z.string(),
@@ -7,30 +7,30 @@ export const userProfileSchema = z.object({
role: z.string(), role: z.string(),
avatarUrl: z.url().nullable(), avatarUrl: z.url().nullable(),
createdAt: z.string(), createdAt: z.string(),
}) });
export type UserProfile = z.infer<typeof userProfileSchema> export type UserProfile = z.infer<typeof userProfileSchema>;
export type UserProfileResponse = UserProfile export type UserProfileResponse = UserProfile;
export const passwordResetStartSchema = z.object({ export const passwordResetStartSchema = z.object({
email: z.string().email('Ingresá un email válido.'), email: z.string().email('Ingresá un email válido.'),
}) });
export type PasswordResetStartInput = z.infer<typeof passwordResetStartSchema> export type PasswordResetStartInput = z.infer<typeof passwordResetStartSchema>;
export const passwordResetVerifySchema = z.object({ export const passwordResetVerifySchema = z.object({
requestId: z.string().uuid('Request ID inválido.'), requestId: z.string().uuid('Request ID inválido.'),
otp: z.string().length(6, 'El código debe tener 6 dígitos.'), otp: z.string().length(6, 'El código debe tener 6 dígitos.'),
newPassword: z.string().min(6, 'La contraseña debe tener al menos 6 caracteres.'), newPassword: z.string().min(6, 'La contraseña debe tener al menos 6 caracteres.'),
}) });
export type PasswordResetVerifyInput = z.infer<typeof passwordResetVerifySchema> export type PasswordResetVerifyInput = z.infer<typeof passwordResetVerifySchema>;
export const passwordResetResendSchema = z.object({ export const passwordResetResendSchema = z.object({
requestId: z.string().uuid('Request ID inválido.'), requestId: z.string().uuid('Request ID inválido.'),
}) });
export type PasswordResetResendInput = z.infer<typeof passwordResetResendSchema> export type PasswordResetResendInput = z.infer<typeof passwordResetResendSchema>;
export type PasswordResetStartResponse = { export type PasswordResetStartResponse = {
message: string; message: string;
@@ -38,11 +38,11 @@ export type PasswordResetStartResponse = {
email: string; email: string;
expiresAt: string; expiresAt: string;
cooldownSeconds: number; cooldownSeconds: number;
} };
export type PasswordResetVerifyResponse = { export type PasswordResetVerifyResponse = {
message: string; message: string;
} };
export type PasswordResetResendResponse = { export type PasswordResetResendResponse = {
message: string; message: string;
@@ -50,4 +50,4 @@ export type PasswordResetResendResponse = {
email: string; email: string;
expiresAt: string; expiresAt: string;
cooldownSeconds: number; cooldownSeconds: number;
} };