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`
3. Consume from `apps/frontend` via workspace import `@repo/api-contract`
## Endpoint Pattern
Every new endpoint must follow the **Result pattern** with the **validate helper**.
### Files
| File | Purpose |
|------|---------|
| `apps/backend/src/lib/result.ts` | `Result<T>` type, `ok()`, `err()` |
| `apps/backend/src/lib/errors.ts` | `AppError` discriminated union, `Errors` factory |
| `apps/backend/src/lib/http/handle-result.ts` | `handleResult()` — bridges `Result` to HTTP |
| `apps/backend/src/lib/http/validate.ts` | `validate.json()`, `.query()`, `.param()`, `.header()`, `.form()` |
### Layers
**1. Route** — Use `validate.json(schema)`, `validate.param(schema)`, etc. as Hono middleware.
```typescript
import { validate } from '@/lib/http/validate';
import { z } from 'zod';
const idParams = z.object({ id: z.string() });
router.post('/', validate.json(createSchema), createHandler);
router.get('/:id', validate.param(idParams), getHandler);
```
**2. Service** — Return `Result<T>`. Use `ok(value)` on success, `err(Errors.*(...))` on failure.
```typescript
import { Result, ok, err } from '@/lib/result';
import { Errors } from '@/lib/errors';
export async function doSomething(input: Input): Promise<Result<Output>> {
if (conflict) return err(Errors.conflict('Already exists'));
return ok(result);
}
```
**3. Handler** — Call the service and pass the `Result` to `handleResult`.
```typescript
import { handleResult } from '@/lib/http/handle-result';
import { doSomething } from './service';
export async function myHandler(c: AppContext) {
const payload = c.req.valid('json') as Input;
return handleResult(c, await doSomething(payload), 201);
}
```
### Never
- ❌ Throw custom error classes from services
- ❌ Use try/catch in handlers for business logic errors
- ❌ Use `zValidator` directly — always use `validate.*`
## Authentication (Better Auth)
The project uses **Better Auth** for session management, replacing the legacy Supabase Auth.

View File

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

View File

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

View File

@@ -14,6 +14,7 @@ model Complex {
users ComplexUser[]
invitations ComplexInvitation[]
courts Court[]
recurringGroups RecurringBookingGroup[]
@@index([planCode])
@@index([complexSlug])

View File

@@ -15,6 +15,11 @@ enum CourtBookingStatus {
NOSHOW
}
enum RecurringBookingGroupStatus {
ACTIVE
CANCELLED
}
model Sport {
id String @id @db.Uuid
name String @unique
@@ -34,6 +39,8 @@ model Court {
name String
slotDurationMinutes Int @map("slot_duration_minutes")
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")
updatedAt DateTime @updatedAt @map("updated_at")
complex Complex @relation(fields: [complexId], references: [id], onDelete: Cascade)
@@ -41,12 +48,29 @@ model Court {
availabilities CourtAvailability[]
priceRules CourtPriceRule[]
bookings CourtBooking[]
maintenances CourtMaintenance[]
recurringGroups RecurringBookingGroup[]
@@index([complexId])
@@index([sportId])
@@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 {
id String @id @db.Uuid
courtId String @map("court_id") @db.Uuid
@@ -77,23 +101,26 @@ model CourtPriceRule {
}
model CourtBooking {
id String @id @db.Uuid
bookingCode String @unique @map("booking_code") @db.VarChar(8)
courtId String @map("court_id") @db.Uuid
bookingDate DateTime @map("booking_date") @db.Date
startTime String @map("start_time") @db.VarChar(5)
endTime String @map("end_time") @db.VarChar(5)
customerName String @map("customer_name") @db.VarChar(120)
customerPhone String @map("customer_phone") @db.VarChar(30)
customerEmail String @map("customer_email") @db.VarChar(254)
status CourtBookingStatus @default(CONFIRMED)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
court Court @relation(fields: [courtId], references: [id], onDelete: Cascade)
id String @id @db.Uuid
bookingCode String @unique @map("booking_code") @db.VarChar(8)
courtId String @map("court_id") @db.Uuid
bookingDate DateTime @map("booking_date") @db.Date
startTime String @map("start_time") @db.VarChar(5)
endTime String @map("end_time") @db.VarChar(5)
customerName String @map("customer_name") @db.VarChar(120)
customerPhone String @map("customer_phone") @db.VarChar(30)
customerEmail String @map("customer_email") @db.VarChar(254)
status CourtBookingStatus @default(CONFIRMED)
recurringGroupId String? @map("recurring_group_id") @db.Uuid
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
court Court @relation(fields: [courtId], references: [id], onDelete: Cascade)
recurringGroup RecurringBookingGroup? @relation(fields: [recurringGroupId], references: [id])
@@unique([courtId, bookingDate, startTime])
@@index([courtId, bookingDate])
@@index([bookingDate])
@@index([recurringGroupId])
@@map("court_bookings")
}
@@ -114,3 +141,28 @@ model CourtBookingLog {
@@map("court_booking_logs")
@@index([courtId, newStatus])
}
model RecurringBookingGroup {
id String @id @db.Uuid
complexId String @map("complex_id") @db.Uuid
courtId String @map("court_id") @db.Uuid
startTime String @map("start_time") @db.VarChar(5)
endTime String @map("end_time") @db.VarChar(5)
dayOfWeek DayOfWeek @map("day_of_week")
startDate DateTime @map("start_date") @db.Date
endDate DateTime? @map("end_date") @db.Date
status RecurringBookingGroupStatus @default(ACTIVE)
customerName String @map("customer_name") @db.VarChar(120)
customerPhone String @map("customer_phone") @db.VarChar(30)
customerEmail String @map("customer_email") @db.VarChar(254)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
court Court @relation(fields: [courtId], references: [id], onDelete: Cascade)
complex Complex @relation(fields: [complexId], references: [id], onDelete: Cascade)
bookings CourtBooking[]
@@index([complexId])
@@index([courtId])
@@index([status])
@@map("recurring_booking_groups")
}

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',
price: '49.00',
rules: {
version: 'v1',
version: 'v2',
limits: {
maxCourts: 2,
maxBookingsPerDay: 80,
@@ -21,6 +21,12 @@ export const planSeeds = [
publicBookingPage: true,
advancedReports: false,
whatsappReminders: false,
fixedSlots: false,
},
pricing: {
overrides: {
AR: { amount: 14999, currency: 'ARS' },
},
},
},
},
@@ -29,7 +35,7 @@ export const planSeeds = [
name: 'Advanced',
price: '99.00',
rules: {
version: 'v1',
version: 'v2',
limits: {
maxCourts: 5,
maxBookingsPerDay: 220,
@@ -46,6 +52,12 @@ export const planSeeds = [
publicBookingPage: true,
advancedReports: true,
whatsappReminders: true,
fixedSlots: true,
},
pricing: {
overrides: {
AR: { amount: 29999, currency: 'ARS' },
},
},
},
},
@@ -54,7 +66,7 @@ export const planSeeds = [
name: 'Enterprise',
price: '199.00',
rules: {
version: 'v1',
version: 'v2',
limits: {
maxCourts: 20,
maxBookingsPerDay: 2000,
@@ -71,6 +83,12 @@ export const planSeeds = [
publicBookingPage: true,
advancedReports: 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
/**
* Model CourtMaintenance
*
*/
export type CourtMaintenance = Prisma.CourtMaintenanceModel
/**
* Model CourtAvailability
*
@@ -82,6 +87,11 @@ export type CourtBooking = Prisma.CourtBookingModel
*
*/
export type CourtBookingLog = Prisma.CourtBookingLogModel
/**
* Model RecurringBookingGroup
*
*/
export type RecurringBookingGroup = Prisma.RecurringBookingGroupModel
/**
* Model PasswordResetRequest
*

View File

@@ -86,6 +86,11 @@ export type Sport = Prisma.SportModel
*
*/
export type Court = Prisma.CourtModel
/**
* Model CourtMaintenance
*
*/
export type CourtMaintenance = Prisma.CourtMaintenanceModel
/**
* Model CourtAvailability
*
@@ -106,6 +111,11 @@ export type CourtBooking = Prisma.CourtBookingModel
*
*/
export type CourtBookingLog = Prisma.CourtBookingLogModel
/**
* Model RecurringBookingGroup
*
*/
export type RecurringBookingGroup = Prisma.RecurringBookingGroupModel
/**
* Model PasswordResetRequest
*

View File

@@ -49,6 +49,17 @@ export type StringNullableFilter<$PrismaModel = never> = {
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> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
@@ -109,6 +120,20 @@ export type StringNullableWithAggregatesFilter<$PrismaModel = never> = {
_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> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
@@ -123,29 +148,31 @@ export type DateTimeWithAggregatesFilter<$PrismaModel = never> = {
_max?: Prisma.NestedDateTimeFilter<$PrismaModel>
}
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 FloatNullableFilter<$PrismaModel = never> = {
equals?: number | Prisma.FloatFieldRefInput<$PrismaModel> | null
in?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel> | null
notIn?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel> | null
lt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
lte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
not?: Prisma.NestedFloatNullableFilter<$PrismaModel> | number | null
}
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
export type FloatNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: number | Prisma.FloatFieldRefInput<$PrismaModel> | null
in?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel> | null
notIn?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel> | null
lt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
lte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
not?: Prisma.NestedFloatNullableWithAggregatesFilter<$PrismaModel> | number | null
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
_min?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
_max?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
_avg?: Prisma.NestedFloatNullableFilter<$PrismaModel>
_sum?: Prisma.NestedFloatNullableFilter<$PrismaModel>
_min?: Prisma.NestedFloatNullableFilter<$PrismaModel>
_max?: Prisma.NestedFloatNullableFilter<$PrismaModel>
}
export type UuidFilter<$PrismaModel = never> = {
@@ -287,6 +314,18 @@ export type EnumCourtBookingStatusFilter<$PrismaModel = never> = {
not?: Prisma.NestedEnumCourtBookingStatusFilter<$PrismaModel> | $Enums.CourtBookingStatus
}
export type UuidNullableFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
mode?: Prisma.QueryMode
not?: Prisma.NestedUuidNullableFilter<$PrismaModel> | string | null
}
export type EnumCourtBookingStatusWithAggregatesFilter<$PrismaModel = never> = {
equals?: $Enums.CourtBookingStatus | Prisma.EnumCourtBookingStatusFieldRefInput<$PrismaModel>
in?: $Enums.CourtBookingStatus[] | Prisma.ListEnumCourtBookingStatusFieldRefInput<$PrismaModel>
@@ -297,6 +336,38 @@ export type EnumCourtBookingStatusWithAggregatesFilter<$PrismaModel = never> = {
_max?: Prisma.NestedEnumCourtBookingStatusFilter<$PrismaModel>
}
export type UuidNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
mode?: Prisma.QueryMode
not?: Prisma.NestedUuidNullableWithAggregatesFilter<$PrismaModel> | string | null
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
_min?: Prisma.NestedStringNullableFilter<$PrismaModel>
_max?: Prisma.NestedStringNullableFilter<$PrismaModel>
}
export type EnumRecurringBookingGroupStatusFilter<$PrismaModel = never> = {
equals?: $Enums.RecurringBookingGroupStatus | Prisma.EnumRecurringBookingGroupStatusFieldRefInput<$PrismaModel>
in?: $Enums.RecurringBookingGroupStatus[] | Prisma.ListEnumRecurringBookingGroupStatusFieldRefInput<$PrismaModel>
notIn?: $Enums.RecurringBookingGroupStatus[] | Prisma.ListEnumRecurringBookingGroupStatusFieldRefInput<$PrismaModel>
not?: Prisma.NestedEnumRecurringBookingGroupStatusFilter<$PrismaModel> | $Enums.RecurringBookingGroupStatus
}
export type EnumRecurringBookingGroupStatusWithAggregatesFilter<$PrismaModel = never> = {
equals?: $Enums.RecurringBookingGroupStatus | Prisma.EnumRecurringBookingGroupStatusFieldRefInput<$PrismaModel>
in?: $Enums.RecurringBookingGroupStatus[] | Prisma.ListEnumRecurringBookingGroupStatusFieldRefInput<$PrismaModel>
notIn?: $Enums.RecurringBookingGroupStatus[] | Prisma.ListEnumRecurringBookingGroupStatusFieldRefInput<$PrismaModel>
not?: Prisma.NestedEnumRecurringBookingGroupStatusWithAggregatesFilter<$PrismaModel> | $Enums.RecurringBookingGroupStatus
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedEnumRecurringBookingGroupStatusFilter<$PrismaModel>
_max?: Prisma.NestedEnumRecurringBookingGroupStatusFilter<$PrismaModel>
}
export type JsonFilter<$PrismaModel = never> =
| Prisma.PatchUndefined<
Prisma.Either<Required<JsonFilterBase<$PrismaModel>>, Exclude<keyof Required<JsonFilterBase<$PrismaModel>>, 'path'>>,
@@ -381,6 +452,17 @@ export type NestedStringNullableFilter<$PrismaModel = never> = {
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> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
@@ -456,6 +538,20 @@ export type NestedIntNullableFilter<$PrismaModel = never> = {
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> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
@@ -470,29 +566,31 @@ export type NestedDateTimeWithAggregatesFilter<$PrismaModel = never> = {
_max?: Prisma.NestedDateTimeFilter<$PrismaModel>
}
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 NestedFloatNullableFilter<$PrismaModel = never> = {
equals?: number | Prisma.FloatFieldRefInput<$PrismaModel> | null
in?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel> | null
notIn?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel> | null
lt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
lte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
not?: Prisma.NestedFloatNullableFilter<$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
export type NestedFloatNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: number | Prisma.FloatFieldRefInput<$PrismaModel> | null
in?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel> | null
notIn?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel> | null
lt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
lte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
not?: Prisma.NestedFloatNullableWithAggregatesFilter<$PrismaModel> | number | null
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
_min?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
_max?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
_avg?: Prisma.NestedFloatNullableFilter<$PrismaModel>
_sum?: Prisma.NestedFloatNullableFilter<$PrismaModel>
_min?: Prisma.NestedFloatNullableFilter<$PrismaModel>
_max?: Prisma.NestedFloatNullableFilter<$PrismaModel>
}
export type NestedUuidFilter<$PrismaModel = never> = {
@@ -632,6 +730,17 @@ export type NestedEnumCourtBookingStatusFilter<$PrismaModel = never> = {
not?: Prisma.NestedEnumCourtBookingStatusFilter<$PrismaModel> | $Enums.CourtBookingStatus
}
export type NestedUuidNullableFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
not?: Prisma.NestedUuidNullableFilter<$PrismaModel> | string | null
}
export type NestedEnumCourtBookingStatusWithAggregatesFilter<$PrismaModel = never> = {
equals?: $Enums.CourtBookingStatus | Prisma.EnumCourtBookingStatusFieldRefInput<$PrismaModel>
in?: $Enums.CourtBookingStatus[] | Prisma.ListEnumCourtBookingStatusFieldRefInput<$PrismaModel>
@@ -642,6 +751,37 @@ export type NestedEnumCourtBookingStatusWithAggregatesFilter<$PrismaModel = neve
_max?: Prisma.NestedEnumCourtBookingStatusFilter<$PrismaModel>
}
export type NestedUuidNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
not?: Prisma.NestedUuidNullableWithAggregatesFilter<$PrismaModel> | string | null
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
_min?: Prisma.NestedStringNullableFilter<$PrismaModel>
_max?: Prisma.NestedStringNullableFilter<$PrismaModel>
}
export type NestedEnumRecurringBookingGroupStatusFilter<$PrismaModel = never> = {
equals?: $Enums.RecurringBookingGroupStatus | Prisma.EnumRecurringBookingGroupStatusFieldRefInput<$PrismaModel>
in?: $Enums.RecurringBookingGroupStatus[] | Prisma.ListEnumRecurringBookingGroupStatusFieldRefInput<$PrismaModel>
notIn?: $Enums.RecurringBookingGroupStatus[] | Prisma.ListEnumRecurringBookingGroupStatusFieldRefInput<$PrismaModel>
not?: Prisma.NestedEnumRecurringBookingGroupStatusFilter<$PrismaModel> | $Enums.RecurringBookingGroupStatus
}
export type NestedEnumRecurringBookingGroupStatusWithAggregatesFilter<$PrismaModel = never> = {
equals?: $Enums.RecurringBookingGroupStatus | Prisma.EnumRecurringBookingGroupStatusFieldRefInput<$PrismaModel>
in?: $Enums.RecurringBookingGroupStatus[] | Prisma.ListEnumRecurringBookingGroupStatusFieldRefInput<$PrismaModel>
notIn?: $Enums.RecurringBookingGroupStatus[] | Prisma.ListEnumRecurringBookingGroupStatusFieldRefInput<$PrismaModel>
not?: Prisma.NestedEnumRecurringBookingGroupStatusWithAggregatesFilter<$PrismaModel> | $Enums.RecurringBookingGroupStatus
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedEnumRecurringBookingGroupStatusFilter<$PrismaModel>
_max?: Prisma.NestedEnumRecurringBookingGroupStatusFilter<$PrismaModel>
}
export type NestedJsonFilter<$PrismaModel = never> =
| Prisma.PatchUndefined<
Prisma.Either<Required<NestedJsonFilterBase<$PrismaModel>>, Exclude<keyof Required<NestedJsonFilterBase<$PrismaModel>>, 'path'>>,

View File

@@ -38,3 +38,11 @@ export const CourtBookingStatus = {
} as const
export type CourtBookingStatus = (typeof CourtBookingStatus)[keyof typeof CourtBookingStatus]
export const RecurringBookingGroupStatus = {
ACTIVE: 'ACTIVE',
CANCELLED: 'CANCELLED'
} as const
export type RecurringBookingGroupStatus = (typeof RecurringBookingGroupStatus)[keyof typeof RecurringBookingGroupStatus]

File diff suppressed because one or more lines are too long

View File

@@ -393,10 +393,12 @@ export const ModelName = {
ComplexInvitation: 'ComplexInvitation',
Sport: 'Sport',
Court: 'Court',
CourtMaintenance: 'CourtMaintenance',
CourtAvailability: 'CourtAvailability',
CourtPriceRule: 'CourtPriceRule',
CourtBooking: 'CourtBooking',
CourtBookingLog: 'CourtBookingLog',
RecurringBookingGroup: 'RecurringBookingGroup',
PasswordResetRequest: 'PasswordResetRequest',
Plan: 'Plan'
} as const
@@ -414,7 +416,7 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
omit: GlobalOmitOptions
}
meta: {
modelProps: "user" | "session" | "account" | "verification" | "complex" | "complexUser" | "complexInvitation" | "sport" | "court" | "courtAvailability" | "courtPriceRule" | "courtBooking" | "courtBookingLog" | "passwordResetRequest" | "plan"
modelProps: "user" | "session" | "account" | "verification" | "complex" | "complexUser" | "complexInvitation" | "sport" | "court" | "courtMaintenance" | "courtAvailability" | "courtPriceRule" | "courtBooking" | "courtBookingLog" | "recurringBookingGroup" | "passwordResetRequest" | "plan"
txIsolationLevel: TransactionIsolationLevel
}
model: {
@@ -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: {
payload: Prisma.$CourtAvailabilityPayload<ExtArgs>
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: {
payload: Prisma.$PasswordResetRequestPayload<ExtArgs>
fields: Prisma.PasswordResetRequestFieldRefs
@@ -1574,6 +1724,9 @@ export const UserScalarFieldEnum = {
emailVerified: 'emailVerified',
image: 'image',
phone: 'phone',
banned: 'banned',
bannedAt: 'bannedAt',
banReason: 'banReason',
createdAt: 'createdAt',
updatedAt: 'updatedAt',
role: 'role'
@@ -1590,6 +1743,11 @@ export const SessionScalarFieldEnum = {
updatedAt: 'updatedAt',
ipAddress: 'ipAddress',
userAgent: 'userAgent',
country: 'country',
city: 'city',
countryCode: 'countryCode',
latitude: 'latitude',
longitude: 'longitude',
userId: 'userId'
} as const
@@ -1688,6 +1846,8 @@ export const CourtScalarFieldEnum = {
name: 'name',
slotDurationMinutes: 'slotDurationMinutes',
basePrice: 'basePrice',
isUnderMaintenance: 'isUnderMaintenance',
maintenanceReason: 'maintenanceReason',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
@@ -1695,6 +1855,20 @@ export const 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 = {
id: 'id',
courtId: 'courtId',
@@ -1733,6 +1907,7 @@ export const CourtBookingScalarFieldEnum = {
customerPhone: 'customerPhone',
customerEmail: 'customerEmail',
status: 'status',
recurringGroupId: 'recurringGroupId',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
@@ -1758,6 +1933,26 @@ export const CourtBookingLogScalarFieldEnum = {
export type CourtBookingLogScalarFieldEnum = (typeof CourtBookingLogScalarFieldEnum)[keyof typeof CourtBookingLogScalarFieldEnum]
export const RecurringBookingGroupScalarFieldEnum = {
id: 'id',
complexId: 'complexId',
courtId: 'courtId',
startTime: 'startTime',
endTime: 'endTime',
dayOfWeek: 'dayOfWeek',
startDate: 'startDate',
endDate: 'endDate',
status: 'status',
customerName: 'customerName',
customerPhone: 'customerPhone',
customerEmail: 'customerEmail',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
export type RecurringBookingGroupScalarFieldEnum = (typeof RecurringBookingGroupScalarFieldEnum)[keyof typeof RecurringBookingGroupScalarFieldEnum]
export const PasswordResetRequestScalarFieldEnum = {
id: 'id',
email: 'email',
@@ -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'
*/
@@ -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'
*/
@@ -1949,20 +2172,6 @@ export type JsonFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'J
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
*/
@@ -2067,10 +2276,12 @@ export type GlobalOmitConfig = {
complexInvitation?: Prisma.ComplexInvitationOmit
sport?: Prisma.SportOmit
court?: Prisma.CourtOmit
courtMaintenance?: Prisma.CourtMaintenanceOmit
courtAvailability?: Prisma.CourtAvailabilityOmit
courtPriceRule?: Prisma.CourtPriceRuleOmit
courtBooking?: Prisma.CourtBookingOmit
courtBookingLog?: Prisma.CourtBookingLogOmit
recurringBookingGroup?: Prisma.RecurringBookingGroupOmit
passwordResetRequest?: Prisma.PasswordResetRequestOmit
plan?: Prisma.PlanOmit
}

View File

@@ -60,10 +60,12 @@ export const ModelName = {
ComplexInvitation: 'ComplexInvitation',
Sport: 'Sport',
Court: 'Court',
CourtMaintenance: 'CourtMaintenance',
CourtAvailability: 'CourtAvailability',
CourtPriceRule: 'CourtPriceRule',
CourtBooking: 'CourtBooking',
CourtBookingLog: 'CourtBookingLog',
RecurringBookingGroup: 'RecurringBookingGroup',
PasswordResetRequest: 'PasswordResetRequest',
Plan: 'Plan'
} as const
@@ -91,6 +93,9 @@ export const UserScalarFieldEnum = {
emailVerified: 'emailVerified',
image: 'image',
phone: 'phone',
banned: 'banned',
bannedAt: 'bannedAt',
banReason: 'banReason',
createdAt: 'createdAt',
updatedAt: 'updatedAt',
role: 'role'
@@ -107,6 +112,11 @@ export const SessionScalarFieldEnum = {
updatedAt: 'updatedAt',
ipAddress: 'ipAddress',
userAgent: 'userAgent',
country: 'country',
city: 'city',
countryCode: 'countryCode',
latitude: 'latitude',
longitude: 'longitude',
userId: 'userId'
} as const
@@ -205,6 +215,8 @@ export const CourtScalarFieldEnum = {
name: 'name',
slotDurationMinutes: 'slotDurationMinutes',
basePrice: 'basePrice',
isUnderMaintenance: 'isUnderMaintenance',
maintenanceReason: 'maintenanceReason',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
@@ -212,6 +224,20 @@ export const 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 = {
id: 'id',
courtId: 'courtId',
@@ -250,6 +276,7 @@ export const CourtBookingScalarFieldEnum = {
customerPhone: 'customerPhone',
customerEmail: 'customerEmail',
status: 'status',
recurringGroupId: 'recurringGroupId',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
@@ -275,6 +302,26 @@ export const CourtBookingLogScalarFieldEnum = {
export type CourtBookingLogScalarFieldEnum = (typeof CourtBookingLogScalarFieldEnum)[keyof typeof CourtBookingLogScalarFieldEnum]
export const RecurringBookingGroupScalarFieldEnum = {
id: 'id',
complexId: 'complexId',
courtId: 'courtId',
startTime: 'startTime',
endTime: 'endTime',
dayOfWeek: 'dayOfWeek',
startDate: 'startDate',
endDate: 'endDate',
status: 'status',
customerName: 'customerName',
customerPhone: 'customerPhone',
customerEmail: 'customerEmail',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
export type RecurringBookingGroupScalarFieldEnum = (typeof RecurringBookingGroupScalarFieldEnum)[keyof typeof RecurringBookingGroupScalarFieldEnum]
export const PasswordResetRequestScalarFieldEnum = {
id: 'id',
email: 'email',

View File

@@ -17,10 +17,12 @@ export type * from './models/ComplexUser'
export type * from './models/ComplexInvitation'
export type * from './models/Sport'
export type * from './models/Court'
export type * from './models/CourtMaintenance'
export type * from './models/CourtAvailability'
export type * from './models/CourtPriceRule'
export type * from './models/CourtBooking'
export type * from './models/CourtBookingLog'
export type * from './models/RecurringBookingGroup'
export type * from './models/PasswordResetRequest'
export type * from './models/Plan'
export type * from './commonInputTypes'

View File

@@ -234,6 +234,7 @@ export type ComplexWhereInput = {
users?: Prisma.ComplexUserListRelationFilter
invitations?: Prisma.ComplexInvitationListRelationFilter
courts?: Prisma.CourtListRelationFilter
recurringGroups?: Prisma.RecurringBookingGroupListRelationFilter
}
export type ComplexOrderByWithRelationInput = {
@@ -252,6 +253,7 @@ export type ComplexOrderByWithRelationInput = {
users?: Prisma.ComplexUserOrderByRelationAggregateInput
invitations?: Prisma.ComplexInvitationOrderByRelationAggregateInput
courts?: Prisma.CourtOrderByRelationAggregateInput
recurringGroups?: Prisma.RecurringBookingGroupOrderByRelationAggregateInput
}
export type ComplexWhereUniqueInput = Prisma.AtLeast<{
@@ -273,6 +275,7 @@ export type ComplexWhereUniqueInput = Prisma.AtLeast<{
users?: Prisma.ComplexUserListRelationFilter
invitations?: Prisma.ComplexInvitationListRelationFilter
courts?: Prisma.CourtListRelationFilter
recurringGroups?: Prisma.RecurringBookingGroupListRelationFilter
}, "id" | "complexSlug">
export type ComplexOrderByWithAggregationInput = {
@@ -324,6 +327,7 @@ export type ComplexCreateInput = {
users?: Prisma.ComplexUserCreateNestedManyWithoutComplexInput
invitations?: Prisma.ComplexInvitationCreateNestedManyWithoutComplexInput
courts?: Prisma.CourtCreateNestedManyWithoutComplexInput
recurringGroups?: Prisma.RecurringBookingGroupCreateNestedManyWithoutComplexInput
}
export type ComplexUncheckedCreateInput = {
@@ -341,6 +345,7 @@ export type ComplexUncheckedCreateInput = {
users?: Prisma.ComplexUserUncheckedCreateNestedManyWithoutComplexInput
invitations?: Prisma.ComplexInvitationUncheckedCreateNestedManyWithoutComplexInput
courts?: Prisma.CourtUncheckedCreateNestedManyWithoutComplexInput
recurringGroups?: Prisma.RecurringBookingGroupUncheckedCreateNestedManyWithoutComplexInput
}
export type ComplexUpdateInput = {
@@ -358,6 +363,7 @@ export type ComplexUpdateInput = {
users?: Prisma.ComplexUserUpdateManyWithoutComplexNestedInput
invitations?: Prisma.ComplexInvitationUpdateManyWithoutComplexNestedInput
courts?: Prisma.CourtUpdateManyWithoutComplexNestedInput
recurringGroups?: Prisma.RecurringBookingGroupUpdateManyWithoutComplexNestedInput
}
export type ComplexUncheckedUpdateInput = {
@@ -375,6 +381,7 @@ export type ComplexUncheckedUpdateInput = {
users?: Prisma.ComplexUserUncheckedUpdateManyWithoutComplexNestedInput
invitations?: Prisma.ComplexInvitationUncheckedUpdateManyWithoutComplexNestedInput
courts?: Prisma.CourtUncheckedUpdateManyWithoutComplexNestedInput
recurringGroups?: Prisma.RecurringBookingGroupUncheckedUpdateManyWithoutComplexNestedInput
}
export type ComplexCreateManyInput = {
@@ -517,6 +524,20 @@ export type ComplexUpdateOneRequiredWithoutCourtsNestedInput = {
update?: Prisma.XOR<Prisma.XOR<Prisma.ComplexUpdateToOneWithWhereWithoutCourtsInput, Prisma.ComplexUpdateWithoutCourtsInput>, Prisma.ComplexUncheckedUpdateWithoutCourtsInput>
}
export type ComplexCreateNestedOneWithoutRecurringGroupsInput = {
create?: Prisma.XOR<Prisma.ComplexCreateWithoutRecurringGroupsInput, Prisma.ComplexUncheckedCreateWithoutRecurringGroupsInput>
connectOrCreate?: Prisma.ComplexCreateOrConnectWithoutRecurringGroupsInput
connect?: Prisma.ComplexWhereUniqueInput
}
export type ComplexUpdateOneRequiredWithoutRecurringGroupsNestedInput = {
create?: Prisma.XOR<Prisma.ComplexCreateWithoutRecurringGroupsInput, Prisma.ComplexUncheckedCreateWithoutRecurringGroupsInput>
connectOrCreate?: Prisma.ComplexCreateOrConnectWithoutRecurringGroupsInput
upsert?: Prisma.ComplexUpsertWithoutRecurringGroupsInput
connect?: Prisma.ComplexWhereUniqueInput
update?: Prisma.XOR<Prisma.XOR<Prisma.ComplexUpdateToOneWithWhereWithoutRecurringGroupsInput, Prisma.ComplexUpdateWithoutRecurringGroupsInput>, Prisma.ComplexUncheckedUpdateWithoutRecurringGroupsInput>
}
export type ComplexCreateNestedManyWithoutPlanInput = {
create?: Prisma.XOR<Prisma.ComplexCreateWithoutPlanInput, Prisma.ComplexUncheckedCreateWithoutPlanInput> | Prisma.ComplexCreateWithoutPlanInput[] | Prisma.ComplexUncheckedCreateWithoutPlanInput[]
connectOrCreate?: Prisma.ComplexCreateOrConnectWithoutPlanInput | Prisma.ComplexCreateOrConnectWithoutPlanInput[]
@@ -573,6 +594,7 @@ export type ComplexCreateWithoutUsersInput = {
plan?: Prisma.PlanCreateNestedOneWithoutComplexesInput
invitations?: Prisma.ComplexInvitationCreateNestedManyWithoutComplexInput
courts?: Prisma.CourtCreateNestedManyWithoutComplexInput
recurringGroups?: Prisma.RecurringBookingGroupCreateNestedManyWithoutComplexInput
}
export type ComplexUncheckedCreateWithoutUsersInput = {
@@ -589,6 +611,7 @@ export type ComplexUncheckedCreateWithoutUsersInput = {
updatedAt?: Date | string
invitations?: Prisma.ComplexInvitationUncheckedCreateNestedManyWithoutComplexInput
courts?: Prisma.CourtUncheckedCreateNestedManyWithoutComplexInput
recurringGroups?: Prisma.RecurringBookingGroupUncheckedCreateNestedManyWithoutComplexInput
}
export type ComplexCreateOrConnectWithoutUsersInput = {
@@ -621,6 +644,7 @@ export type ComplexUpdateWithoutUsersInput = {
plan?: Prisma.PlanUpdateOneWithoutComplexesNestedInput
invitations?: Prisma.ComplexInvitationUpdateManyWithoutComplexNestedInput
courts?: Prisma.CourtUpdateManyWithoutComplexNestedInput
recurringGroups?: Prisma.RecurringBookingGroupUpdateManyWithoutComplexNestedInput
}
export type ComplexUncheckedUpdateWithoutUsersInput = {
@@ -637,6 +661,7 @@ export type ComplexUncheckedUpdateWithoutUsersInput = {
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
invitations?: Prisma.ComplexInvitationUncheckedUpdateManyWithoutComplexNestedInput
courts?: Prisma.CourtUncheckedUpdateManyWithoutComplexNestedInput
recurringGroups?: Prisma.RecurringBookingGroupUncheckedUpdateManyWithoutComplexNestedInput
}
export type ComplexCreateWithoutInvitationsInput = {
@@ -653,6 +678,7 @@ export type ComplexCreateWithoutInvitationsInput = {
plan?: Prisma.PlanCreateNestedOneWithoutComplexesInput
users?: Prisma.ComplexUserCreateNestedManyWithoutComplexInput
courts?: Prisma.CourtCreateNestedManyWithoutComplexInput
recurringGroups?: Prisma.RecurringBookingGroupCreateNestedManyWithoutComplexInput
}
export type ComplexUncheckedCreateWithoutInvitationsInput = {
@@ -669,6 +695,7 @@ export type ComplexUncheckedCreateWithoutInvitationsInput = {
updatedAt?: Date | string
users?: Prisma.ComplexUserUncheckedCreateNestedManyWithoutComplexInput
courts?: Prisma.CourtUncheckedCreateNestedManyWithoutComplexInput
recurringGroups?: Prisma.RecurringBookingGroupUncheckedCreateNestedManyWithoutComplexInput
}
export type ComplexCreateOrConnectWithoutInvitationsInput = {
@@ -701,6 +728,7 @@ export type ComplexUpdateWithoutInvitationsInput = {
plan?: Prisma.PlanUpdateOneWithoutComplexesNestedInput
users?: Prisma.ComplexUserUpdateManyWithoutComplexNestedInput
courts?: Prisma.CourtUpdateManyWithoutComplexNestedInput
recurringGroups?: Prisma.RecurringBookingGroupUpdateManyWithoutComplexNestedInput
}
export type ComplexUncheckedUpdateWithoutInvitationsInput = {
@@ -717,6 +745,7 @@ export type ComplexUncheckedUpdateWithoutInvitationsInput = {
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
users?: Prisma.ComplexUserUncheckedUpdateManyWithoutComplexNestedInput
courts?: Prisma.CourtUncheckedUpdateManyWithoutComplexNestedInput
recurringGroups?: Prisma.RecurringBookingGroupUncheckedUpdateManyWithoutComplexNestedInput
}
export type ComplexCreateWithoutCourtsInput = {
@@ -733,6 +762,7 @@ export type ComplexCreateWithoutCourtsInput = {
plan?: Prisma.PlanCreateNestedOneWithoutComplexesInput
users?: Prisma.ComplexUserCreateNestedManyWithoutComplexInput
invitations?: Prisma.ComplexInvitationCreateNestedManyWithoutComplexInput
recurringGroups?: Prisma.RecurringBookingGroupCreateNestedManyWithoutComplexInput
}
export type ComplexUncheckedCreateWithoutCourtsInput = {
@@ -749,6 +779,7 @@ export type ComplexUncheckedCreateWithoutCourtsInput = {
updatedAt?: Date | string
users?: Prisma.ComplexUserUncheckedCreateNestedManyWithoutComplexInput
invitations?: Prisma.ComplexInvitationUncheckedCreateNestedManyWithoutComplexInput
recurringGroups?: Prisma.RecurringBookingGroupUncheckedCreateNestedManyWithoutComplexInput
}
export type ComplexCreateOrConnectWithoutCourtsInput = {
@@ -781,6 +812,7 @@ export type ComplexUpdateWithoutCourtsInput = {
plan?: Prisma.PlanUpdateOneWithoutComplexesNestedInput
users?: Prisma.ComplexUserUpdateManyWithoutComplexNestedInput
invitations?: Prisma.ComplexInvitationUpdateManyWithoutComplexNestedInput
recurringGroups?: Prisma.RecurringBookingGroupUpdateManyWithoutComplexNestedInput
}
export type ComplexUncheckedUpdateWithoutCourtsInput = {
@@ -797,6 +829,91 @@ export type ComplexUncheckedUpdateWithoutCourtsInput = {
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
users?: Prisma.ComplexUserUncheckedUpdateManyWithoutComplexNestedInput
invitations?: Prisma.ComplexInvitationUncheckedUpdateManyWithoutComplexNestedInput
recurringGroups?: Prisma.RecurringBookingGroupUncheckedUpdateManyWithoutComplexNestedInput
}
export type ComplexCreateWithoutRecurringGroupsInput = {
id: string
complexName: string
physicalAddress?: string | null
city?: string | null
state?: string | null
country?: string | null
complexSlug: string
adminEmail: string
createdAt?: Date | string
updatedAt?: Date | string
plan?: Prisma.PlanCreateNestedOneWithoutComplexesInput
users?: Prisma.ComplexUserCreateNestedManyWithoutComplexInput
invitations?: Prisma.ComplexInvitationCreateNestedManyWithoutComplexInput
courts?: Prisma.CourtCreateNestedManyWithoutComplexInput
}
export type ComplexUncheckedCreateWithoutRecurringGroupsInput = {
id: string
complexName: string
physicalAddress?: string | null
city?: string | null
state?: string | null
country?: string | null
complexSlug: string
adminEmail: string
planCode?: string | null
createdAt?: Date | string
updatedAt?: Date | string
users?: Prisma.ComplexUserUncheckedCreateNestedManyWithoutComplexInput
invitations?: Prisma.ComplexInvitationUncheckedCreateNestedManyWithoutComplexInput
courts?: Prisma.CourtUncheckedCreateNestedManyWithoutComplexInput
}
export type ComplexCreateOrConnectWithoutRecurringGroupsInput = {
where: Prisma.ComplexWhereUniqueInput
create: Prisma.XOR<Prisma.ComplexCreateWithoutRecurringGroupsInput, Prisma.ComplexUncheckedCreateWithoutRecurringGroupsInput>
}
export type ComplexUpsertWithoutRecurringGroupsInput = {
update: Prisma.XOR<Prisma.ComplexUpdateWithoutRecurringGroupsInput, Prisma.ComplexUncheckedUpdateWithoutRecurringGroupsInput>
create: Prisma.XOR<Prisma.ComplexCreateWithoutRecurringGroupsInput, Prisma.ComplexUncheckedCreateWithoutRecurringGroupsInput>
where?: Prisma.ComplexWhereInput
}
export type ComplexUpdateToOneWithWhereWithoutRecurringGroupsInput = {
where?: Prisma.ComplexWhereInput
data: Prisma.XOR<Prisma.ComplexUpdateWithoutRecurringGroupsInput, Prisma.ComplexUncheckedUpdateWithoutRecurringGroupsInput>
}
export type ComplexUpdateWithoutRecurringGroupsInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
complexName?: Prisma.StringFieldUpdateOperationsInput | string
physicalAddress?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
city?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
state?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
country?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
complexSlug?: Prisma.StringFieldUpdateOperationsInput | string
adminEmail?: Prisma.StringFieldUpdateOperationsInput | string
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
plan?: Prisma.PlanUpdateOneWithoutComplexesNestedInput
users?: Prisma.ComplexUserUpdateManyWithoutComplexNestedInput
invitations?: Prisma.ComplexInvitationUpdateManyWithoutComplexNestedInput
courts?: Prisma.CourtUpdateManyWithoutComplexNestedInput
}
export type ComplexUncheckedUpdateWithoutRecurringGroupsInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
complexName?: Prisma.StringFieldUpdateOperationsInput | string
physicalAddress?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
city?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
state?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
country?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
complexSlug?: Prisma.StringFieldUpdateOperationsInput | string
adminEmail?: Prisma.StringFieldUpdateOperationsInput | string
planCode?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
users?: Prisma.ComplexUserUncheckedUpdateManyWithoutComplexNestedInput
invitations?: Prisma.ComplexInvitationUncheckedUpdateManyWithoutComplexNestedInput
courts?: Prisma.CourtUncheckedUpdateManyWithoutComplexNestedInput
}
export type ComplexCreateWithoutPlanInput = {
@@ -813,6 +930,7 @@ export type ComplexCreateWithoutPlanInput = {
users?: Prisma.ComplexUserCreateNestedManyWithoutComplexInput
invitations?: Prisma.ComplexInvitationCreateNestedManyWithoutComplexInput
courts?: Prisma.CourtCreateNestedManyWithoutComplexInput
recurringGroups?: Prisma.RecurringBookingGroupCreateNestedManyWithoutComplexInput
}
export type ComplexUncheckedCreateWithoutPlanInput = {
@@ -829,6 +947,7 @@ export type ComplexUncheckedCreateWithoutPlanInput = {
users?: Prisma.ComplexUserUncheckedCreateNestedManyWithoutComplexInput
invitations?: Prisma.ComplexInvitationUncheckedCreateNestedManyWithoutComplexInput
courts?: Prisma.CourtUncheckedCreateNestedManyWithoutComplexInput
recurringGroups?: Prisma.RecurringBookingGroupUncheckedCreateNestedManyWithoutComplexInput
}
export type ComplexCreateOrConnectWithoutPlanInput = {
@@ -901,6 +1020,7 @@ export type ComplexUpdateWithoutPlanInput = {
users?: Prisma.ComplexUserUpdateManyWithoutComplexNestedInput
invitations?: Prisma.ComplexInvitationUpdateManyWithoutComplexNestedInput
courts?: Prisma.CourtUpdateManyWithoutComplexNestedInput
recurringGroups?: Prisma.RecurringBookingGroupUpdateManyWithoutComplexNestedInput
}
export type ComplexUncheckedUpdateWithoutPlanInput = {
@@ -917,6 +1037,7 @@ export type ComplexUncheckedUpdateWithoutPlanInput = {
users?: Prisma.ComplexUserUncheckedUpdateManyWithoutComplexNestedInput
invitations?: Prisma.ComplexInvitationUncheckedUpdateManyWithoutComplexNestedInput
courts?: Prisma.CourtUncheckedUpdateManyWithoutComplexNestedInput
recurringGroups?: Prisma.RecurringBookingGroupUncheckedUpdateManyWithoutComplexNestedInput
}
export type ComplexUncheckedUpdateManyWithoutPlanInput = {
@@ -941,12 +1062,14 @@ export type ComplexCountOutputType = {
users: number
invitations: number
courts: number
recurringGroups: number
}
export type ComplexCountOutputTypeSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
users?: boolean | ComplexCountOutputTypeCountUsersArgs
invitations?: boolean | ComplexCountOutputTypeCountInvitationsArgs
courts?: boolean | ComplexCountOutputTypeCountCourtsArgs
recurringGroups?: boolean | ComplexCountOutputTypeCountRecurringGroupsArgs
}
/**
@@ -980,6 +1103,13 @@ export type ComplexCountOutputTypeCountCourtsArgs<ExtArgs extends runtime.Types.
where?: Prisma.CourtWhereInput
}
/**
* ComplexCountOutputType without action
*/
export type ComplexCountOutputTypeCountRecurringGroupsArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
where?: Prisma.RecurringBookingGroupWhereInput
}
export type ComplexSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean
@@ -997,6 +1127,7 @@ export type ComplexSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs
users?: boolean | Prisma.Complex$usersArgs<ExtArgs>
invitations?: boolean | Prisma.Complex$invitationsArgs<ExtArgs>
courts?: boolean | Prisma.Complex$courtsArgs<ExtArgs>
recurringGroups?: boolean | Prisma.Complex$recurringGroupsArgs<ExtArgs>
_count?: boolean | Prisma.ComplexCountOutputTypeDefaultArgs<ExtArgs>
}, ExtArgs["result"]["complex"]>
@@ -1050,6 +1181,7 @@ export type ComplexInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs
users?: boolean | Prisma.Complex$usersArgs<ExtArgs>
invitations?: boolean | Prisma.Complex$invitationsArgs<ExtArgs>
courts?: boolean | Prisma.Complex$courtsArgs<ExtArgs>
recurringGroups?: boolean | Prisma.Complex$recurringGroupsArgs<ExtArgs>
_count?: boolean | Prisma.ComplexCountOutputTypeDefaultArgs<ExtArgs>
}
export type ComplexIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
@@ -1066,6 +1198,7 @@ export type $ComplexPayload<ExtArgs extends runtime.Types.Extensions.InternalArg
users: Prisma.$ComplexUserPayload<ExtArgs>[]
invitations: Prisma.$ComplexInvitationPayload<ExtArgs>[]
courts: Prisma.$CourtPayload<ExtArgs>[]
recurringGroups: Prisma.$RecurringBookingGroupPayload<ExtArgs>[]
}
scalars: runtime.Types.Extensions.GetPayloadResult<{
id: string
@@ -1477,6 +1610,7 @@ export interface Prisma__ComplexClient<T, Null = never, ExtArgs extends runtime.
users<T extends Prisma.Complex$usersArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Complex$usersArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$ComplexUserPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
invitations<T extends Prisma.Complex$invitationsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Complex$invitationsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$ComplexInvitationPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
courts<T extends Prisma.Complex$courtsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Complex$courtsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$CourtPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
recurringGroups<T extends Prisma.Complex$recurringGroupsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Complex$recurringGroupsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$RecurringBookingGroupPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
/**
* Attaches callbacks for the resolution and/or rejection of the Promise.
* @param onfulfilled The callback to execute when the Promise is resolved.
@@ -2008,6 +2142,30 @@ export type Complex$courtsArgs<ExtArgs extends runtime.Types.Extensions.Internal
distinct?: Prisma.CourtScalarFieldEnum | Prisma.CourtScalarFieldEnum[]
}
/**
* Complex.recurringGroups
*/
export type Complex$recurringGroupsArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
/**
* Select specific fields to fetch from the RecurringBookingGroup
*/
select?: Prisma.RecurringBookingGroupSelect<ExtArgs> | null
/**
* Omit specific fields from the RecurringBookingGroup
*/
omit?: Prisma.RecurringBookingGroupOmit<ExtArgs> | null
/**
* Choose, which related nodes to fetch as well
*/
include?: Prisma.RecurringBookingGroupInclude<ExtArgs> | null
where?: Prisma.RecurringBookingGroupWhereInput
orderBy?: Prisma.RecurringBookingGroupOrderByWithRelationInput | Prisma.RecurringBookingGroupOrderByWithRelationInput[]
cursor?: Prisma.RecurringBookingGroupWhereUniqueInput
take?: number
skip?: number
distinct?: Prisma.RecurringBookingGroupScalarFieldEnum | Prisma.RecurringBookingGroupScalarFieldEnum[]
}
/**
* Complex without action
*/

View File

@@ -43,6 +43,8 @@ export type CourtMinAggregateOutputType = {
name: string | null
slotDurationMinutes: number | null
basePrice: runtime.Decimal | null
isUnderMaintenance: boolean | null
maintenanceReason: string | null
createdAt: Date | null
updatedAt: Date | null
}
@@ -54,6 +56,8 @@ export type CourtMaxAggregateOutputType = {
name: string | null
slotDurationMinutes: number | null
basePrice: runtime.Decimal | null
isUnderMaintenance: boolean | null
maintenanceReason: string | null
createdAt: Date | null
updatedAt: Date | null
}
@@ -65,6 +69,8 @@ export type CourtCountAggregateOutputType = {
name: number
slotDurationMinutes: number
basePrice: number
isUnderMaintenance: number
maintenanceReason: number
createdAt: number
updatedAt: number
_all: number
@@ -88,6 +94,8 @@ export type CourtMinAggregateInputType = {
name?: true
slotDurationMinutes?: true
basePrice?: true
isUnderMaintenance?: true
maintenanceReason?: true
createdAt?: true
updatedAt?: true
}
@@ -99,6 +107,8 @@ export type CourtMaxAggregateInputType = {
name?: true
slotDurationMinutes?: true
basePrice?: true
isUnderMaintenance?: true
maintenanceReason?: true
createdAt?: true
updatedAt?: true
}
@@ -110,6 +120,8 @@ export type CourtCountAggregateInputType = {
name?: true
slotDurationMinutes?: true
basePrice?: true
isUnderMaintenance?: true
maintenanceReason?: true
createdAt?: true
updatedAt?: true
_all?: true
@@ -208,6 +220,8 @@ export type CourtGroupByOutputType = {
name: string
slotDurationMinutes: number
basePrice: runtime.Decimal
isUnderMaintenance: boolean
maintenanceReason: string | null
createdAt: Date
updatedAt: Date
_count: CourtCountAggregateOutputType | null
@@ -242,6 +256,8 @@ export type CourtWhereInput = {
name?: Prisma.StringFilter<"Court"> | string
slotDurationMinutes?: Prisma.IntFilter<"Court"> | number
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
updatedAt?: Prisma.DateTimeFilter<"Court"> | Date | string
complex?: Prisma.XOR<Prisma.ComplexScalarRelationFilter, Prisma.ComplexWhereInput>
@@ -249,6 +265,8 @@ export type CourtWhereInput = {
availabilities?: Prisma.CourtAvailabilityListRelationFilter
priceRules?: Prisma.CourtPriceRuleListRelationFilter
bookings?: Prisma.CourtBookingListRelationFilter
maintenances?: Prisma.CourtMaintenanceListRelationFilter
recurringGroups?: Prisma.RecurringBookingGroupListRelationFilter
}
export type CourtOrderByWithRelationInput = {
@@ -258,6 +276,8 @@ export type CourtOrderByWithRelationInput = {
name?: Prisma.SortOrder
slotDurationMinutes?: Prisma.SortOrder
basePrice?: Prisma.SortOrder
isUnderMaintenance?: Prisma.SortOrder
maintenanceReason?: Prisma.SortOrderInput | Prisma.SortOrder
createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder
complex?: Prisma.ComplexOrderByWithRelationInput
@@ -265,6 +285,8 @@ export type CourtOrderByWithRelationInput = {
availabilities?: Prisma.CourtAvailabilityOrderByRelationAggregateInput
priceRules?: Prisma.CourtPriceRuleOrderByRelationAggregateInput
bookings?: Prisma.CourtBookingOrderByRelationAggregateInput
maintenances?: Prisma.CourtMaintenanceOrderByRelationAggregateInput
recurringGroups?: Prisma.RecurringBookingGroupOrderByRelationAggregateInput
}
export type CourtWhereUniqueInput = Prisma.AtLeast<{
@@ -277,6 +299,8 @@ export type CourtWhereUniqueInput = Prisma.AtLeast<{
name?: Prisma.StringFilter<"Court"> | string
slotDurationMinutes?: Prisma.IntFilter<"Court"> | number
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
updatedAt?: Prisma.DateTimeFilter<"Court"> | Date | string
complex?: Prisma.XOR<Prisma.ComplexScalarRelationFilter, Prisma.ComplexWhereInput>
@@ -284,6 +308,8 @@ export type CourtWhereUniqueInput = Prisma.AtLeast<{
availabilities?: Prisma.CourtAvailabilityListRelationFilter
priceRules?: Prisma.CourtPriceRuleListRelationFilter
bookings?: Prisma.CourtBookingListRelationFilter
maintenances?: Prisma.CourtMaintenanceListRelationFilter
recurringGroups?: Prisma.RecurringBookingGroupListRelationFilter
}, "id">
export type CourtOrderByWithAggregationInput = {
@@ -293,6 +319,8 @@ export type CourtOrderByWithAggregationInput = {
name?: Prisma.SortOrder
slotDurationMinutes?: Prisma.SortOrder
basePrice?: Prisma.SortOrder
isUnderMaintenance?: Prisma.SortOrder
maintenanceReason?: Prisma.SortOrderInput | Prisma.SortOrder
createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder
_count?: Prisma.CourtCountOrderByAggregateInput
@@ -312,6 +340,8 @@ export type CourtScalarWhereWithAggregatesInput = {
name?: Prisma.StringWithAggregatesFilter<"Court"> | string
slotDurationMinutes?: Prisma.IntWithAggregatesFilter<"Court"> | number
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
updatedAt?: Prisma.DateTimeWithAggregatesFilter<"Court"> | Date | string
}
@@ -321,6 +351,8 @@ export type CourtCreateInput = {
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
@@ -328,6 +360,8 @@ export type CourtCreateInput = {
availabilities?: Prisma.CourtAvailabilityCreateNestedManyWithoutCourtInput
priceRules?: Prisma.CourtPriceRuleCreateNestedManyWithoutCourtInput
bookings?: Prisma.CourtBookingCreateNestedManyWithoutCourtInput
maintenances?: Prisma.CourtMaintenanceCreateNestedManyWithoutCourtInput
recurringGroups?: Prisma.RecurringBookingGroupCreateNestedManyWithoutCourtInput
}
export type CourtUncheckedCreateInput = {
@@ -337,11 +371,15 @@ export type CourtUncheckedCreateInput = {
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
recurringGroups?: Prisma.RecurringBookingGroupUncheckedCreateNestedManyWithoutCourtInput
}
export type CourtUpdateInput = {
@@ -349,6 +387,8 @@ export type CourtUpdateInput = {
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
@@ -356,6 +396,8 @@ export type CourtUpdateInput = {
availabilities?: Prisma.CourtAvailabilityUpdateManyWithoutCourtNestedInput
priceRules?: Prisma.CourtPriceRuleUpdateManyWithoutCourtNestedInput
bookings?: Prisma.CourtBookingUpdateManyWithoutCourtNestedInput
maintenances?: Prisma.CourtMaintenanceUpdateManyWithoutCourtNestedInput
recurringGroups?: Prisma.RecurringBookingGroupUpdateManyWithoutCourtNestedInput
}
export type CourtUncheckedUpdateInput = {
@@ -365,11 +407,15 @@ export type CourtUncheckedUpdateInput = {
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
recurringGroups?: Prisma.RecurringBookingGroupUncheckedUpdateManyWithoutCourtNestedInput
}
export type CourtCreateManyInput = {
@@ -379,6 +425,8 @@ export type CourtCreateManyInput = {
name: string
slotDurationMinutes: number
basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string
isUnderMaintenance?: boolean
maintenanceReason?: string | null
createdAt?: Date | string
updatedAt?: Date | string
}
@@ -388,6 +436,8 @@ export type CourtUpdateManyMutationInput = {
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
}
@@ -399,6 +449,8 @@ export type CourtUncheckedUpdateManyInput = {
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
}
@@ -420,6 +472,8 @@ export type CourtCountOrderByAggregateInput = {
name?: Prisma.SortOrder
slotDurationMinutes?: Prisma.SortOrder
basePrice?: Prisma.SortOrder
isUnderMaintenance?: Prisma.SortOrder
maintenanceReason?: Prisma.SortOrder
createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder
}
@@ -436,6 +490,8 @@ export type CourtMaxOrderByAggregateInput = {
name?: Prisma.SortOrder
slotDurationMinutes?: Prisma.SortOrder
basePrice?: Prisma.SortOrder
isUnderMaintenance?: Prisma.SortOrder
maintenanceReason?: Prisma.SortOrder
createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder
}
@@ -447,6 +503,8 @@ export type CourtMinOrderByAggregateInput = {
name?: Prisma.SortOrder
slotDurationMinutes?: Prisma.SortOrder
basePrice?: Prisma.SortOrder
isUnderMaintenance?: Prisma.SortOrder
maintenanceReason?: Prisma.SortOrder
createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder
}
@@ -561,6 +619,20 @@ export type DecimalFieldUpdateOperationsInput = {
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 = {
create?: Prisma.XOR<Prisma.CourtCreateWithoutAvailabilitiesInput, Prisma.CourtUncheckedCreateWithoutAvailabilitiesInput>
connectOrCreate?: Prisma.CourtCreateOrConnectWithoutAvailabilitiesInput
@@ -603,17 +675,35 @@ export type CourtUpdateOneRequiredWithoutBookingsNestedInput = {
update?: Prisma.XOR<Prisma.XOR<Prisma.CourtUpdateToOneWithWhereWithoutBookingsInput, Prisma.CourtUpdateWithoutBookingsInput>, Prisma.CourtUncheckedUpdateWithoutBookingsInput>
}
export type CourtCreateNestedOneWithoutRecurringGroupsInput = {
create?: Prisma.XOR<Prisma.CourtCreateWithoutRecurringGroupsInput, Prisma.CourtUncheckedCreateWithoutRecurringGroupsInput>
connectOrCreate?: Prisma.CourtCreateOrConnectWithoutRecurringGroupsInput
connect?: Prisma.CourtWhereUniqueInput
}
export type CourtUpdateOneRequiredWithoutRecurringGroupsNestedInput = {
create?: Prisma.XOR<Prisma.CourtCreateWithoutRecurringGroupsInput, Prisma.CourtUncheckedCreateWithoutRecurringGroupsInput>
connectOrCreate?: Prisma.CourtCreateOrConnectWithoutRecurringGroupsInput
upsert?: Prisma.CourtUpsertWithoutRecurringGroupsInput
connect?: Prisma.CourtWhereUniqueInput
update?: Prisma.XOR<Prisma.XOR<Prisma.CourtUpdateToOneWithWhereWithoutRecurringGroupsInput, Prisma.CourtUpdateWithoutRecurringGroupsInput>, Prisma.CourtUncheckedUpdateWithoutRecurringGroupsInput>
}
export type CourtCreateWithoutComplexInput = {
id: string
name: string
slotDurationMinutes: number
basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string
isUnderMaintenance?: boolean
maintenanceReason?: string | null
createdAt?: Date | string
updatedAt?: Date | string
sport: Prisma.SportCreateNestedOneWithoutCourtsInput
availabilities?: Prisma.CourtAvailabilityCreateNestedManyWithoutCourtInput
priceRules?: Prisma.CourtPriceRuleCreateNestedManyWithoutCourtInput
bookings?: Prisma.CourtBookingCreateNestedManyWithoutCourtInput
maintenances?: Prisma.CourtMaintenanceCreateNestedManyWithoutCourtInput
recurringGroups?: Prisma.RecurringBookingGroupCreateNestedManyWithoutCourtInput
}
export type CourtUncheckedCreateWithoutComplexInput = {
@@ -622,11 +712,15 @@ export type CourtUncheckedCreateWithoutComplexInput = {
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
recurringGroups?: Prisma.RecurringBookingGroupUncheckedCreateNestedManyWithoutCourtInput
}
export type CourtCreateOrConnectWithoutComplexInput = {
@@ -665,6 +759,8 @@ export type CourtScalarWhereInput = {
name?: Prisma.StringFilter<"Court"> | string
slotDurationMinutes?: Prisma.IntFilter<"Court"> | number
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
updatedAt?: Prisma.DateTimeFilter<"Court"> | Date | string
}
@@ -674,12 +770,16 @@ export type CourtCreateWithoutSportInput = {
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
availabilities?: Prisma.CourtAvailabilityCreateNestedManyWithoutCourtInput
priceRules?: Prisma.CourtPriceRuleCreateNestedManyWithoutCourtInput
bookings?: Prisma.CourtBookingCreateNestedManyWithoutCourtInput
maintenances?: Prisma.CourtMaintenanceCreateNestedManyWithoutCourtInput
recurringGroups?: Prisma.RecurringBookingGroupCreateNestedManyWithoutCourtInput
}
export type CourtUncheckedCreateWithoutSportInput = {
@@ -688,11 +788,15 @@ export type CourtUncheckedCreateWithoutSportInput = {
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
recurringGroups?: Prisma.RecurringBookingGroupUncheckedCreateNestedManyWithoutCourtInput
}
export type CourtCreateOrConnectWithoutSportInput = {
@@ -721,17 +825,105 @@ export type CourtUpdateManyWithWhereWithoutSportInput = {
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 = {
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
priceRules?: Prisma.CourtPriceRuleCreateNestedManyWithoutCourtInput
bookings?: Prisma.CourtBookingCreateNestedManyWithoutCourtInput
maintenances?: Prisma.CourtMaintenanceCreateNestedManyWithoutCourtInput
recurringGroups?: Prisma.RecurringBookingGroupCreateNestedManyWithoutCourtInput
}
export type CourtUncheckedCreateWithoutAvailabilitiesInput = {
@@ -741,10 +933,14 @@ export type CourtUncheckedCreateWithoutAvailabilitiesInput = {
name: string
slotDurationMinutes: number
basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string
isUnderMaintenance?: boolean
maintenanceReason?: string | null
createdAt?: Date | string
updatedAt?: Date | string
priceRules?: Prisma.CourtPriceRuleUncheckedCreateNestedManyWithoutCourtInput
bookings?: Prisma.CourtBookingUncheckedCreateNestedManyWithoutCourtInput
maintenances?: Prisma.CourtMaintenanceUncheckedCreateNestedManyWithoutCourtInput
recurringGroups?: Prisma.RecurringBookingGroupUncheckedCreateNestedManyWithoutCourtInput
}
export type CourtCreateOrConnectWithoutAvailabilitiesInput = {
@@ -768,12 +964,16 @@ export type CourtUpdateWithoutAvailabilitiesInput = {
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
priceRules?: Prisma.CourtPriceRuleUpdateManyWithoutCourtNestedInput
bookings?: Prisma.CourtBookingUpdateManyWithoutCourtNestedInput
maintenances?: Prisma.CourtMaintenanceUpdateManyWithoutCourtNestedInput
recurringGroups?: Prisma.RecurringBookingGroupUpdateManyWithoutCourtNestedInput
}
export type CourtUncheckedUpdateWithoutAvailabilitiesInput = {
@@ -783,10 +983,14 @@ export type CourtUncheckedUpdateWithoutAvailabilitiesInput = {
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
priceRules?: Prisma.CourtPriceRuleUncheckedUpdateManyWithoutCourtNestedInput
bookings?: Prisma.CourtBookingUncheckedUpdateManyWithoutCourtNestedInput
maintenances?: Prisma.CourtMaintenanceUncheckedUpdateManyWithoutCourtNestedInput
recurringGroups?: Prisma.RecurringBookingGroupUncheckedUpdateManyWithoutCourtNestedInput
}
export type CourtCreateWithoutPriceRulesInput = {
@@ -794,12 +998,16 @@ export type CourtCreateWithoutPriceRulesInput = {
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
bookings?: Prisma.CourtBookingCreateNestedManyWithoutCourtInput
maintenances?: Prisma.CourtMaintenanceCreateNestedManyWithoutCourtInput
recurringGroups?: Prisma.RecurringBookingGroupCreateNestedManyWithoutCourtInput
}
export type CourtUncheckedCreateWithoutPriceRulesInput = {
@@ -809,10 +1017,14 @@ export type CourtUncheckedCreateWithoutPriceRulesInput = {
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
bookings?: Prisma.CourtBookingUncheckedCreateNestedManyWithoutCourtInput
maintenances?: Prisma.CourtMaintenanceUncheckedCreateNestedManyWithoutCourtInput
recurringGroups?: Prisma.RecurringBookingGroupUncheckedCreateNestedManyWithoutCourtInput
}
export type CourtCreateOrConnectWithoutPriceRulesInput = {
@@ -836,12 +1048,16 @@ export type CourtUpdateWithoutPriceRulesInput = {
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
bookings?: Prisma.CourtBookingUpdateManyWithoutCourtNestedInput
maintenances?: Prisma.CourtMaintenanceUpdateManyWithoutCourtNestedInput
recurringGroups?: Prisma.RecurringBookingGroupUpdateManyWithoutCourtNestedInput
}
export type CourtUncheckedUpdateWithoutPriceRulesInput = {
@@ -851,10 +1067,14 @@ export type CourtUncheckedUpdateWithoutPriceRulesInput = {
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
bookings?: Prisma.CourtBookingUncheckedUpdateManyWithoutCourtNestedInput
maintenances?: Prisma.CourtMaintenanceUncheckedUpdateManyWithoutCourtNestedInput
recurringGroups?: Prisma.RecurringBookingGroupUncheckedUpdateManyWithoutCourtNestedInput
}
export type CourtCreateWithoutBookingsInput = {
@@ -862,12 +1082,16 @@ export type CourtCreateWithoutBookingsInput = {
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
maintenances?: Prisma.CourtMaintenanceCreateNestedManyWithoutCourtInput
recurringGroups?: Prisma.RecurringBookingGroupCreateNestedManyWithoutCourtInput
}
export type CourtUncheckedCreateWithoutBookingsInput = {
@@ -877,10 +1101,14 @@ export type CourtUncheckedCreateWithoutBookingsInput = {
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
maintenances?: Prisma.CourtMaintenanceUncheckedCreateNestedManyWithoutCourtInput
recurringGroups?: Prisma.RecurringBookingGroupUncheckedCreateNestedManyWithoutCourtInput
}
export type CourtCreateOrConnectWithoutBookingsInput = {
@@ -904,12 +1132,16 @@ export type CourtUpdateWithoutBookingsInput = {
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
maintenances?: Prisma.CourtMaintenanceUpdateManyWithoutCourtNestedInput
recurringGroups?: Prisma.RecurringBookingGroupUpdateManyWithoutCourtNestedInput
}
export type CourtUncheckedUpdateWithoutBookingsInput = {
@@ -919,10 +1151,98 @@ export type CourtUncheckedUpdateWithoutBookingsInput = {
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
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 = {
@@ -931,6 +1251,8 @@ export type CourtCreateManyComplexInput = {
name: string
slotDurationMinutes: number
basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string
isUnderMaintenance?: boolean
maintenanceReason?: string | null
createdAt?: Date | string
updatedAt?: Date | string
}
@@ -940,12 +1262,16 @@ export type CourtUpdateWithoutComplexInput = {
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
sport?: Prisma.SportUpdateOneRequiredWithoutCourtsNestedInput
availabilities?: Prisma.CourtAvailabilityUpdateManyWithoutCourtNestedInput
priceRules?: Prisma.CourtPriceRuleUpdateManyWithoutCourtNestedInput
bookings?: Prisma.CourtBookingUpdateManyWithoutCourtNestedInput
maintenances?: Prisma.CourtMaintenanceUpdateManyWithoutCourtNestedInput
recurringGroups?: Prisma.RecurringBookingGroupUpdateManyWithoutCourtNestedInput
}
export type CourtUncheckedUpdateWithoutComplexInput = {
@@ -954,11 +1280,15 @@ export type CourtUncheckedUpdateWithoutComplexInput = {
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
recurringGroups?: Prisma.RecurringBookingGroupUncheckedUpdateManyWithoutCourtNestedInput
}
export type CourtUncheckedUpdateManyWithoutComplexInput = {
@@ -967,6 +1297,8 @@ export type CourtUncheckedUpdateManyWithoutComplexInput = {
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
}
@@ -977,6 +1309,8 @@ export type CourtCreateManySportInput = {
name: string
slotDurationMinutes: number
basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string
isUnderMaintenance?: boolean
maintenanceReason?: string | null
createdAt?: Date | string
updatedAt?: Date | string
}
@@ -986,12 +1320,16 @@ export type CourtUpdateWithoutSportInput = {
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
availabilities?: Prisma.CourtAvailabilityUpdateManyWithoutCourtNestedInput
priceRules?: Prisma.CourtPriceRuleUpdateManyWithoutCourtNestedInput
bookings?: Prisma.CourtBookingUpdateManyWithoutCourtNestedInput
maintenances?: Prisma.CourtMaintenanceUpdateManyWithoutCourtNestedInput
recurringGroups?: Prisma.RecurringBookingGroupUpdateManyWithoutCourtNestedInput
}
export type CourtUncheckedUpdateWithoutSportInput = {
@@ -1000,11 +1338,15 @@ export type CourtUncheckedUpdateWithoutSportInput = {
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
recurringGroups?: Prisma.RecurringBookingGroupUncheckedUpdateManyWithoutCourtNestedInput
}
export type CourtUncheckedUpdateManyWithoutSportInput = {
@@ -1013,6 +1355,8 @@ export type CourtUncheckedUpdateManyWithoutSportInput = {
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
}
@@ -1026,12 +1370,16 @@ export type CourtCountOutputType = {
availabilities: number
priceRules: number
bookings: number
maintenances: number
recurringGroups: number
}
export type CourtCountOutputTypeSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
availabilities?: boolean | CourtCountOutputTypeCountAvailabilitiesArgs
priceRules?: boolean | CourtCountOutputTypeCountPriceRulesArgs
bookings?: boolean | CourtCountOutputTypeCountBookingsArgs
maintenances?: boolean | CourtCountOutputTypeCountMaintenancesArgs
recurringGroups?: boolean | CourtCountOutputTypeCountRecurringGroupsArgs
}
/**
@@ -1065,6 +1413,20 @@ export type CourtCountOutputTypeCountBookingsArgs<ExtArgs extends runtime.Types.
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<{
id?: boolean
@@ -1073,6 +1435,8 @@ export type CourtSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs =
name?: boolean
slotDurationMinutes?: boolean
basePrice?: boolean
isUnderMaintenance?: boolean
maintenanceReason?: boolean
createdAt?: boolean
updatedAt?: boolean
complex?: boolean | Prisma.ComplexDefaultArgs<ExtArgs>
@@ -1080,6 +1444,8 @@ export type CourtSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs =
availabilities?: boolean | Prisma.Court$availabilitiesArgs<ExtArgs>
priceRules?: boolean | Prisma.Court$priceRulesArgs<ExtArgs>
bookings?: boolean | Prisma.Court$bookingsArgs<ExtArgs>
maintenances?: boolean | Prisma.Court$maintenancesArgs<ExtArgs>
recurringGroups?: boolean | Prisma.Court$recurringGroupsArgs<ExtArgs>
_count?: boolean | Prisma.CourtCountOutputTypeDefaultArgs<ExtArgs>
}, ExtArgs["result"]["court"]>
@@ -1090,6 +1456,8 @@ export type CourtSelectCreateManyAndReturn<ExtArgs extends runtime.Types.Extensi
name?: boolean
slotDurationMinutes?: boolean
basePrice?: boolean
isUnderMaintenance?: boolean
maintenanceReason?: boolean
createdAt?: boolean
updatedAt?: boolean
complex?: boolean | Prisma.ComplexDefaultArgs<ExtArgs>
@@ -1103,6 +1471,8 @@ export type CourtSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensi
name?: boolean
slotDurationMinutes?: boolean
basePrice?: boolean
isUnderMaintenance?: boolean
maintenanceReason?: boolean
createdAt?: boolean
updatedAt?: boolean
complex?: boolean | Prisma.ComplexDefaultArgs<ExtArgs>
@@ -1116,17 +1486,21 @@ export type CourtSelectScalar = {
name?: boolean
slotDurationMinutes?: boolean
basePrice?: boolean
isUnderMaintenance?: boolean
maintenanceReason?: boolean
createdAt?: 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> = {
complex?: boolean | Prisma.ComplexDefaultArgs<ExtArgs>
sport?: boolean | Prisma.SportDefaultArgs<ExtArgs>
availabilities?: boolean | Prisma.Court$availabilitiesArgs<ExtArgs>
priceRules?: boolean | Prisma.Court$priceRulesArgs<ExtArgs>
bookings?: boolean | Prisma.Court$bookingsArgs<ExtArgs>
maintenances?: boolean | Prisma.Court$maintenancesArgs<ExtArgs>
recurringGroups?: boolean | Prisma.Court$recurringGroupsArgs<ExtArgs>
_count?: boolean | Prisma.CourtCountOutputTypeDefaultArgs<ExtArgs>
}
export type CourtIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
@@ -1146,6 +1520,8 @@ export type $CourtPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs
availabilities: Prisma.$CourtAvailabilityPayload<ExtArgs>[]
priceRules: Prisma.$CourtPriceRulePayload<ExtArgs>[]
bookings: Prisma.$CourtBookingPayload<ExtArgs>[]
maintenances: Prisma.$CourtMaintenancePayload<ExtArgs>[]
recurringGroups: Prisma.$RecurringBookingGroupPayload<ExtArgs>[]
}
scalars: runtime.Types.Extensions.GetPayloadResult<{
id: string
@@ -1154,6 +1530,8 @@ export type $CourtPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs
name: string
slotDurationMinutes: number
basePrice: runtime.Decimal
isUnderMaintenance: boolean
maintenanceReason: string | null
createdAt: Date
updatedAt: Date
}, 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>
priceRules<T extends Prisma.Court$priceRulesArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Court$priceRulesArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$CourtPriceRulePayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
bookings<T extends Prisma.Court$bookingsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Court$bookingsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$CourtBookingPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
maintenances<T extends Prisma.Court$maintenancesArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Court$maintenancesArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$CourtMaintenancePayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
recurringGroups<T extends Prisma.Court$recurringGroupsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Court$recurringGroupsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$RecurringBookingGroupPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
/**
* Attaches callbacks for the resolution and/or rejection of the Promise.
* @param onfulfilled The callback to execute when the Promise is resolved.
@@ -1590,6 +1970,8 @@ export interface CourtFieldRefs {
readonly name: Prisma.FieldRef<"Court", 'String'>
readonly slotDurationMinutes: Prisma.FieldRef<"Court", 'Int'>
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 updatedAt: Prisma.FieldRef<"Court", 'DateTime'>
}
@@ -2064,6 +2446,54 @@ export type Court$bookingsArgs<ExtArgs extends runtime.Types.Extensions.Internal
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
*/

View File

@@ -35,6 +35,7 @@ export type CourtBookingMinAggregateOutputType = {
customerPhone: string | null
customerEmail: string | null
status: $Enums.CourtBookingStatus | null
recurringGroupId: string | null
createdAt: Date | null
updatedAt: Date | null
}
@@ -50,6 +51,7 @@ export type CourtBookingMaxAggregateOutputType = {
customerPhone: string | null
customerEmail: string | null
status: $Enums.CourtBookingStatus | null
recurringGroupId: string | null
createdAt: Date | null
updatedAt: Date | null
}
@@ -65,6 +67,7 @@ export type CourtBookingCountAggregateOutputType = {
customerPhone: number
customerEmail: number
status: number
recurringGroupId: number
createdAt: number
updatedAt: number
_all: number
@@ -82,6 +85,7 @@ export type CourtBookingMinAggregateInputType = {
customerPhone?: true
customerEmail?: true
status?: true
recurringGroupId?: true
createdAt?: true
updatedAt?: true
}
@@ -97,6 +101,7 @@ export type CourtBookingMaxAggregateInputType = {
customerPhone?: true
customerEmail?: true
status?: true
recurringGroupId?: true
createdAt?: true
updatedAt?: true
}
@@ -112,6 +117,7 @@ export type CourtBookingCountAggregateInputType = {
customerPhone?: true
customerEmail?: true
status?: true
recurringGroupId?: true
createdAt?: true
updatedAt?: true
_all?: true
@@ -200,6 +206,7 @@ export type CourtBookingGroupByOutputType = {
customerPhone: string
customerEmail: string
status: $Enums.CourtBookingStatus
recurringGroupId: string | null
createdAt: Date
updatedAt: Date
_count: CourtBookingCountAggregateOutputType | null
@@ -236,9 +243,11 @@ export type CourtBookingWhereInput = {
customerPhone?: Prisma.StringFilter<"CourtBooking"> | string
customerEmail?: Prisma.StringFilter<"CourtBooking"> | string
status?: Prisma.EnumCourtBookingStatusFilter<"CourtBooking"> | $Enums.CourtBookingStatus
recurringGroupId?: Prisma.UuidNullableFilter<"CourtBooking"> | string | null
createdAt?: Prisma.DateTimeFilter<"CourtBooking"> | Date | string
updatedAt?: Prisma.DateTimeFilter<"CourtBooking"> | Date | string
court?: Prisma.XOR<Prisma.CourtScalarRelationFilter, Prisma.CourtWhereInput>
recurringGroup?: Prisma.XOR<Prisma.RecurringBookingGroupNullableScalarRelationFilter, Prisma.RecurringBookingGroupWhereInput> | null
}
export type CourtBookingOrderByWithRelationInput = {
@@ -252,9 +261,11 @@ export type CourtBookingOrderByWithRelationInput = {
customerPhone?: Prisma.SortOrder
customerEmail?: Prisma.SortOrder
status?: Prisma.SortOrder
recurringGroupId?: Prisma.SortOrderInput | Prisma.SortOrder
createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder
court?: Prisma.CourtOrderByWithRelationInput
recurringGroup?: Prisma.RecurringBookingGroupOrderByWithRelationInput
}
export type CourtBookingWhereUniqueInput = Prisma.AtLeast<{
@@ -272,9 +283,11 @@ export type CourtBookingWhereUniqueInput = Prisma.AtLeast<{
customerPhone?: Prisma.StringFilter<"CourtBooking"> | string
customerEmail?: Prisma.StringFilter<"CourtBooking"> | string
status?: Prisma.EnumCourtBookingStatusFilter<"CourtBooking"> | $Enums.CourtBookingStatus
recurringGroupId?: Prisma.UuidNullableFilter<"CourtBooking"> | string | null
createdAt?: Prisma.DateTimeFilter<"CourtBooking"> | Date | string
updatedAt?: Prisma.DateTimeFilter<"CourtBooking"> | Date | string
court?: Prisma.XOR<Prisma.CourtScalarRelationFilter, Prisma.CourtWhereInput>
recurringGroup?: Prisma.XOR<Prisma.RecurringBookingGroupNullableScalarRelationFilter, Prisma.RecurringBookingGroupWhereInput> | null
}, "id" | "bookingCode" | "courtId_bookingDate_startTime">
export type CourtBookingOrderByWithAggregationInput = {
@@ -288,6 +301,7 @@ export type CourtBookingOrderByWithAggregationInput = {
customerPhone?: Prisma.SortOrder
customerEmail?: Prisma.SortOrder
status?: Prisma.SortOrder
recurringGroupId?: Prisma.SortOrderInput | Prisma.SortOrder
createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder
_count?: Prisma.CourtBookingCountOrderByAggregateInput
@@ -309,6 +323,7 @@ export type CourtBookingScalarWhereWithAggregatesInput = {
customerPhone?: Prisma.StringWithAggregatesFilter<"CourtBooking"> | string
customerEmail?: Prisma.StringWithAggregatesFilter<"CourtBooking"> | string
status?: Prisma.EnumCourtBookingStatusWithAggregatesFilter<"CourtBooking"> | $Enums.CourtBookingStatus
recurringGroupId?: Prisma.UuidNullableWithAggregatesFilter<"CourtBooking"> | string | null
createdAt?: Prisma.DateTimeWithAggregatesFilter<"CourtBooking"> | Date | string
updatedAt?: Prisma.DateTimeWithAggregatesFilter<"CourtBooking"> | Date | string
}
@@ -326,6 +341,7 @@ export type CourtBookingCreateInput = {
createdAt?: Date | string
updatedAt?: Date | string
court: Prisma.CourtCreateNestedOneWithoutBookingsInput
recurringGroup?: Prisma.RecurringBookingGroupCreateNestedOneWithoutBookingsInput
}
export type CourtBookingUncheckedCreateInput = {
@@ -339,6 +355,7 @@ export type CourtBookingUncheckedCreateInput = {
customerPhone: string
customerEmail: string
status?: $Enums.CourtBookingStatus
recurringGroupId?: string | null
createdAt?: Date | string
updatedAt?: Date | string
}
@@ -356,6 +373,7 @@ export type CourtBookingUpdateInput = {
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
court?: Prisma.CourtUpdateOneRequiredWithoutBookingsNestedInput
recurringGroup?: Prisma.RecurringBookingGroupUpdateOneWithoutBookingsNestedInput
}
export type CourtBookingUncheckedUpdateInput = {
@@ -369,6 +387,7 @@ export type CourtBookingUncheckedUpdateInput = {
customerPhone?: Prisma.StringFieldUpdateOperationsInput | string
customerEmail?: Prisma.StringFieldUpdateOperationsInput | string
status?: Prisma.EnumCourtBookingStatusFieldUpdateOperationsInput | $Enums.CourtBookingStatus
recurringGroupId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
}
@@ -384,6 +403,7 @@ export type CourtBookingCreateManyInput = {
customerPhone: string
customerEmail: string
status?: $Enums.CourtBookingStatus
recurringGroupId?: string | null
createdAt?: Date | string
updatedAt?: Date | string
}
@@ -413,6 +433,7 @@ export type CourtBookingUncheckedUpdateManyInput = {
customerPhone?: Prisma.StringFieldUpdateOperationsInput | string
customerEmail?: Prisma.StringFieldUpdateOperationsInput | string
status?: Prisma.EnumCourtBookingStatusFieldUpdateOperationsInput | $Enums.CourtBookingStatus
recurringGroupId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
}
@@ -444,6 +465,7 @@ export type CourtBookingCountOrderByAggregateInput = {
customerPhone?: Prisma.SortOrder
customerEmail?: Prisma.SortOrder
status?: Prisma.SortOrder
recurringGroupId?: Prisma.SortOrder
createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder
}
@@ -459,6 +481,7 @@ export type CourtBookingMaxOrderByAggregateInput = {
customerPhone?: Prisma.SortOrder
customerEmail?: Prisma.SortOrder
status?: Prisma.SortOrder
recurringGroupId?: Prisma.SortOrder
createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder
}
@@ -474,6 +497,7 @@ export type CourtBookingMinOrderByAggregateInput = {
customerPhone?: Prisma.SortOrder
customerEmail?: Prisma.SortOrder
status?: Prisma.SortOrder
recurringGroupId?: Prisma.SortOrder
createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder
}
@@ -524,6 +548,48 @@ export type EnumCourtBookingStatusFieldUpdateOperationsInput = {
set?: $Enums.CourtBookingStatus
}
export type CourtBookingCreateNestedManyWithoutRecurringGroupInput = {
create?: Prisma.XOR<Prisma.CourtBookingCreateWithoutRecurringGroupInput, Prisma.CourtBookingUncheckedCreateWithoutRecurringGroupInput> | Prisma.CourtBookingCreateWithoutRecurringGroupInput[] | Prisma.CourtBookingUncheckedCreateWithoutRecurringGroupInput[]
connectOrCreate?: Prisma.CourtBookingCreateOrConnectWithoutRecurringGroupInput | Prisma.CourtBookingCreateOrConnectWithoutRecurringGroupInput[]
createMany?: Prisma.CourtBookingCreateManyRecurringGroupInputEnvelope
connect?: Prisma.CourtBookingWhereUniqueInput | Prisma.CourtBookingWhereUniqueInput[]
}
export type CourtBookingUncheckedCreateNestedManyWithoutRecurringGroupInput = {
create?: Prisma.XOR<Prisma.CourtBookingCreateWithoutRecurringGroupInput, Prisma.CourtBookingUncheckedCreateWithoutRecurringGroupInput> | Prisma.CourtBookingCreateWithoutRecurringGroupInput[] | Prisma.CourtBookingUncheckedCreateWithoutRecurringGroupInput[]
connectOrCreate?: Prisma.CourtBookingCreateOrConnectWithoutRecurringGroupInput | Prisma.CourtBookingCreateOrConnectWithoutRecurringGroupInput[]
createMany?: Prisma.CourtBookingCreateManyRecurringGroupInputEnvelope
connect?: Prisma.CourtBookingWhereUniqueInput | Prisma.CourtBookingWhereUniqueInput[]
}
export type CourtBookingUpdateManyWithoutRecurringGroupNestedInput = {
create?: Prisma.XOR<Prisma.CourtBookingCreateWithoutRecurringGroupInput, Prisma.CourtBookingUncheckedCreateWithoutRecurringGroupInput> | Prisma.CourtBookingCreateWithoutRecurringGroupInput[] | Prisma.CourtBookingUncheckedCreateWithoutRecurringGroupInput[]
connectOrCreate?: Prisma.CourtBookingCreateOrConnectWithoutRecurringGroupInput | Prisma.CourtBookingCreateOrConnectWithoutRecurringGroupInput[]
upsert?: Prisma.CourtBookingUpsertWithWhereUniqueWithoutRecurringGroupInput | Prisma.CourtBookingUpsertWithWhereUniqueWithoutRecurringGroupInput[]
createMany?: Prisma.CourtBookingCreateManyRecurringGroupInputEnvelope
set?: Prisma.CourtBookingWhereUniqueInput | Prisma.CourtBookingWhereUniqueInput[]
disconnect?: Prisma.CourtBookingWhereUniqueInput | Prisma.CourtBookingWhereUniqueInput[]
delete?: Prisma.CourtBookingWhereUniqueInput | Prisma.CourtBookingWhereUniqueInput[]
connect?: Prisma.CourtBookingWhereUniqueInput | Prisma.CourtBookingWhereUniqueInput[]
update?: Prisma.CourtBookingUpdateWithWhereUniqueWithoutRecurringGroupInput | Prisma.CourtBookingUpdateWithWhereUniqueWithoutRecurringGroupInput[]
updateMany?: Prisma.CourtBookingUpdateManyWithWhereWithoutRecurringGroupInput | Prisma.CourtBookingUpdateManyWithWhereWithoutRecurringGroupInput[]
deleteMany?: Prisma.CourtBookingScalarWhereInput | Prisma.CourtBookingScalarWhereInput[]
}
export type CourtBookingUncheckedUpdateManyWithoutRecurringGroupNestedInput = {
create?: Prisma.XOR<Prisma.CourtBookingCreateWithoutRecurringGroupInput, Prisma.CourtBookingUncheckedCreateWithoutRecurringGroupInput> | Prisma.CourtBookingCreateWithoutRecurringGroupInput[] | Prisma.CourtBookingUncheckedCreateWithoutRecurringGroupInput[]
connectOrCreate?: Prisma.CourtBookingCreateOrConnectWithoutRecurringGroupInput | Prisma.CourtBookingCreateOrConnectWithoutRecurringGroupInput[]
upsert?: Prisma.CourtBookingUpsertWithWhereUniqueWithoutRecurringGroupInput | Prisma.CourtBookingUpsertWithWhereUniqueWithoutRecurringGroupInput[]
createMany?: Prisma.CourtBookingCreateManyRecurringGroupInputEnvelope
set?: Prisma.CourtBookingWhereUniqueInput | Prisma.CourtBookingWhereUniqueInput[]
disconnect?: Prisma.CourtBookingWhereUniqueInput | Prisma.CourtBookingWhereUniqueInput[]
delete?: Prisma.CourtBookingWhereUniqueInput | Prisma.CourtBookingWhereUniqueInput[]
connect?: Prisma.CourtBookingWhereUniqueInput | Prisma.CourtBookingWhereUniqueInput[]
update?: Prisma.CourtBookingUpdateWithWhereUniqueWithoutRecurringGroupInput | Prisma.CourtBookingUpdateWithWhereUniqueWithoutRecurringGroupInput[]
updateMany?: Prisma.CourtBookingUpdateManyWithWhereWithoutRecurringGroupInput | Prisma.CourtBookingUpdateManyWithWhereWithoutRecurringGroupInput[]
deleteMany?: Prisma.CourtBookingScalarWhereInput | Prisma.CourtBookingScalarWhereInput[]
}
export type CourtBookingCreateWithoutCourtInput = {
id: string
bookingCode: string
@@ -536,6 +602,7 @@ export type CourtBookingCreateWithoutCourtInput = {
status?: $Enums.CourtBookingStatus
createdAt?: Date | string
updatedAt?: Date | string
recurringGroup?: Prisma.RecurringBookingGroupCreateNestedOneWithoutBookingsInput
}
export type CourtBookingUncheckedCreateWithoutCourtInput = {
@@ -548,6 +615,7 @@ export type CourtBookingUncheckedCreateWithoutCourtInput = {
customerPhone: string
customerEmail: string
status?: $Enums.CourtBookingStatus
recurringGroupId?: string | null
createdAt?: Date | string
updatedAt?: Date | string
}
@@ -592,10 +660,67 @@ export type CourtBookingScalarWhereInput = {
customerPhone?: Prisma.StringFilter<"CourtBooking"> | string
customerEmail?: Prisma.StringFilter<"CourtBooking"> | string
status?: Prisma.EnumCourtBookingStatusFilter<"CourtBooking"> | $Enums.CourtBookingStatus
recurringGroupId?: Prisma.UuidNullableFilter<"CourtBooking"> | string | null
createdAt?: Prisma.DateTimeFilter<"CourtBooking"> | Date | string
updatedAt?: Prisma.DateTimeFilter<"CourtBooking"> | Date | string
}
export type CourtBookingCreateWithoutRecurringGroupInput = {
id: string
bookingCode: string
bookingDate: Date | string
startTime: string
endTime: string
customerName: string
customerPhone: string
customerEmail: string
status?: $Enums.CourtBookingStatus
createdAt?: Date | string
updatedAt?: Date | string
court: Prisma.CourtCreateNestedOneWithoutBookingsInput
}
export type CourtBookingUncheckedCreateWithoutRecurringGroupInput = {
id: string
bookingCode: string
courtId: string
bookingDate: Date | string
startTime: string
endTime: string
customerName: string
customerPhone: string
customerEmail: string
status?: $Enums.CourtBookingStatus
createdAt?: Date | string
updatedAt?: Date | string
}
export type CourtBookingCreateOrConnectWithoutRecurringGroupInput = {
where: Prisma.CourtBookingWhereUniqueInput
create: Prisma.XOR<Prisma.CourtBookingCreateWithoutRecurringGroupInput, Prisma.CourtBookingUncheckedCreateWithoutRecurringGroupInput>
}
export type CourtBookingCreateManyRecurringGroupInputEnvelope = {
data: Prisma.CourtBookingCreateManyRecurringGroupInput | Prisma.CourtBookingCreateManyRecurringGroupInput[]
skipDuplicates?: boolean
}
export type CourtBookingUpsertWithWhereUniqueWithoutRecurringGroupInput = {
where: Prisma.CourtBookingWhereUniqueInput
update: Prisma.XOR<Prisma.CourtBookingUpdateWithoutRecurringGroupInput, Prisma.CourtBookingUncheckedUpdateWithoutRecurringGroupInput>
create: Prisma.XOR<Prisma.CourtBookingCreateWithoutRecurringGroupInput, Prisma.CourtBookingUncheckedCreateWithoutRecurringGroupInput>
}
export type CourtBookingUpdateWithWhereUniqueWithoutRecurringGroupInput = {
where: Prisma.CourtBookingWhereUniqueInput
data: Prisma.XOR<Prisma.CourtBookingUpdateWithoutRecurringGroupInput, Prisma.CourtBookingUncheckedUpdateWithoutRecurringGroupInput>
}
export type CourtBookingUpdateManyWithWhereWithoutRecurringGroupInput = {
where: Prisma.CourtBookingScalarWhereInput
data: Prisma.XOR<Prisma.CourtBookingUpdateManyMutationInput, Prisma.CourtBookingUncheckedUpdateManyWithoutRecurringGroupInput>
}
export type CourtBookingCreateManyCourtInput = {
id: string
bookingCode: string
@@ -606,6 +731,7 @@ export type CourtBookingCreateManyCourtInput = {
customerPhone: string
customerEmail: string
status?: $Enums.CourtBookingStatus
recurringGroupId?: string | null
createdAt?: Date | string
updatedAt?: Date | string
}
@@ -622,6 +748,7 @@ export type CourtBookingUpdateWithoutCourtInput = {
status?: Prisma.EnumCourtBookingStatusFieldUpdateOperationsInput | $Enums.CourtBookingStatus
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
recurringGroup?: Prisma.RecurringBookingGroupUpdateOneWithoutBookingsNestedInput
}
export type CourtBookingUncheckedUpdateWithoutCourtInput = {
@@ -634,6 +761,7 @@ export type CourtBookingUncheckedUpdateWithoutCourtInput = {
customerPhone?: Prisma.StringFieldUpdateOperationsInput | string
customerEmail?: Prisma.StringFieldUpdateOperationsInput | string
status?: Prisma.EnumCourtBookingStatusFieldUpdateOperationsInput | $Enums.CourtBookingStatus
recurringGroupId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
}
@@ -648,6 +776,67 @@ export type CourtBookingUncheckedUpdateManyWithoutCourtInput = {
customerPhone?: Prisma.StringFieldUpdateOperationsInput | string
customerEmail?: Prisma.StringFieldUpdateOperationsInput | string
status?: Prisma.EnumCourtBookingStatusFieldUpdateOperationsInput | $Enums.CourtBookingStatus
recurringGroupId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
}
export type CourtBookingCreateManyRecurringGroupInput = {
id: string
bookingCode: string
courtId: string
bookingDate: Date | string
startTime: string
endTime: string
customerName: string
customerPhone: string
customerEmail: string
status?: $Enums.CourtBookingStatus
createdAt?: Date | string
updatedAt?: Date | string
}
export type CourtBookingUpdateWithoutRecurringGroupInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
bookingCode?: Prisma.StringFieldUpdateOperationsInput | string
bookingDate?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
startTime?: Prisma.StringFieldUpdateOperationsInput | string
endTime?: Prisma.StringFieldUpdateOperationsInput | string
customerName?: Prisma.StringFieldUpdateOperationsInput | string
customerPhone?: Prisma.StringFieldUpdateOperationsInput | string
customerEmail?: Prisma.StringFieldUpdateOperationsInput | string
status?: Prisma.EnumCourtBookingStatusFieldUpdateOperationsInput | $Enums.CourtBookingStatus
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
court?: Prisma.CourtUpdateOneRequiredWithoutBookingsNestedInput
}
export type CourtBookingUncheckedUpdateWithoutRecurringGroupInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
bookingCode?: Prisma.StringFieldUpdateOperationsInput | string
courtId?: Prisma.StringFieldUpdateOperationsInput | string
bookingDate?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
startTime?: Prisma.StringFieldUpdateOperationsInput | string
endTime?: Prisma.StringFieldUpdateOperationsInput | string
customerName?: Prisma.StringFieldUpdateOperationsInput | string
customerPhone?: Prisma.StringFieldUpdateOperationsInput | string
customerEmail?: Prisma.StringFieldUpdateOperationsInput | string
status?: Prisma.EnumCourtBookingStatusFieldUpdateOperationsInput | $Enums.CourtBookingStatus
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
}
export type CourtBookingUncheckedUpdateManyWithoutRecurringGroupInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
bookingCode?: Prisma.StringFieldUpdateOperationsInput | string
courtId?: Prisma.StringFieldUpdateOperationsInput | string
bookingDate?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
startTime?: Prisma.StringFieldUpdateOperationsInput | string
endTime?: Prisma.StringFieldUpdateOperationsInput | string
customerName?: Prisma.StringFieldUpdateOperationsInput | string
customerPhone?: Prisma.StringFieldUpdateOperationsInput | string
customerEmail?: Prisma.StringFieldUpdateOperationsInput | string
status?: Prisma.EnumCourtBookingStatusFieldUpdateOperationsInput | $Enums.CourtBookingStatus
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
}
@@ -665,9 +854,11 @@ export type CourtBookingSelect<ExtArgs extends runtime.Types.Extensions.Internal
customerPhone?: boolean
customerEmail?: boolean
status?: boolean
recurringGroupId?: boolean
createdAt?: boolean
updatedAt?: boolean
court?: boolean | Prisma.CourtDefaultArgs<ExtArgs>
recurringGroup?: boolean | Prisma.CourtBooking$recurringGroupArgs<ExtArgs>
}, ExtArgs["result"]["courtBooking"]>
export type CourtBookingSelectCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
@@ -681,9 +872,11 @@ export type CourtBookingSelectCreateManyAndReturn<ExtArgs extends runtime.Types.
customerPhone?: boolean
customerEmail?: boolean
status?: boolean
recurringGroupId?: boolean
createdAt?: boolean
updatedAt?: boolean
court?: boolean | Prisma.CourtDefaultArgs<ExtArgs>
recurringGroup?: boolean | Prisma.CourtBooking$recurringGroupArgs<ExtArgs>
}, ExtArgs["result"]["courtBooking"]>
export type CourtBookingSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
@@ -697,9 +890,11 @@ export type CourtBookingSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.
customerPhone?: boolean
customerEmail?: boolean
status?: boolean
recurringGroupId?: boolean
createdAt?: boolean
updatedAt?: boolean
court?: boolean | Prisma.CourtDefaultArgs<ExtArgs>
recurringGroup?: boolean | Prisma.CourtBooking$recurringGroupArgs<ExtArgs>
}, ExtArgs["result"]["courtBooking"]>
export type CourtBookingSelectScalar = {
@@ -713,25 +908,30 @@ export type CourtBookingSelectScalar = {
customerPhone?: boolean
customerEmail?: boolean
status?: boolean
recurringGroupId?: boolean
createdAt?: boolean
updatedAt?: boolean
}
export type CourtBookingOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "bookingCode" | "courtId" | "bookingDate" | "startTime" | "endTime" | "customerName" | "customerPhone" | "customerEmail" | "status" | "createdAt" | "updatedAt", ExtArgs["result"]["courtBooking"]>
export type CourtBookingOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "bookingCode" | "courtId" | "bookingDate" | "startTime" | "endTime" | "customerName" | "customerPhone" | "customerEmail" | "status" | "recurringGroupId" | "createdAt" | "updatedAt", ExtArgs["result"]["courtBooking"]>
export type CourtBookingInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
court?: boolean | Prisma.CourtDefaultArgs<ExtArgs>
recurringGroup?: boolean | Prisma.CourtBooking$recurringGroupArgs<ExtArgs>
}
export type CourtBookingIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
court?: boolean | Prisma.CourtDefaultArgs<ExtArgs>
recurringGroup?: boolean | Prisma.CourtBooking$recurringGroupArgs<ExtArgs>
}
export type CourtBookingIncludeUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
court?: boolean | Prisma.CourtDefaultArgs<ExtArgs>
recurringGroup?: boolean | Prisma.CourtBooking$recurringGroupArgs<ExtArgs>
}
export type $CourtBookingPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
name: "CourtBooking"
objects: {
court: Prisma.$CourtPayload<ExtArgs>
recurringGroup: Prisma.$RecurringBookingGroupPayload<ExtArgs> | null
}
scalars: runtime.Types.Extensions.GetPayloadResult<{
id: string
@@ -744,6 +944,7 @@ export type $CourtBookingPayload<ExtArgs extends runtime.Types.Extensions.Intern
customerPhone: string
customerEmail: string
status: $Enums.CourtBookingStatus
recurringGroupId: string | null
createdAt: Date
updatedAt: Date
}, ExtArgs["result"]["courtBooking"]>
@@ -1141,6 +1342,7 @@ readonly fields: CourtBookingFieldRefs;
export interface Prisma__CourtBookingClient<T, Null = never, ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs, GlobalOmitOptions = {}> extends Prisma.PrismaPromise<T> {
readonly [Symbol.toStringTag]: "PrismaPromise"
court<T extends Prisma.CourtDefaultArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.CourtDefaultArgs<ExtArgs>>): Prisma.Prisma__CourtClient<runtime.Types.Result.GetResult<Prisma.$CourtPayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | Null, Null, ExtArgs, GlobalOmitOptions>
recurringGroup<T extends Prisma.CourtBooking$recurringGroupArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.CourtBooking$recurringGroupArgs<ExtArgs>>): Prisma.Prisma__RecurringBookingGroupClient<runtime.Types.Result.GetResult<Prisma.$RecurringBookingGroupPayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions>
/**
* Attaches callbacks for the resolution and/or rejection of the Promise.
* @param onfulfilled The callback to execute when the Promise is resolved.
@@ -1180,6 +1382,7 @@ export interface CourtBookingFieldRefs {
readonly customerPhone: Prisma.FieldRef<"CourtBooking", 'String'>
readonly customerEmail: Prisma.FieldRef<"CourtBooking", 'String'>
readonly status: Prisma.FieldRef<"CourtBooking", 'CourtBookingStatus'>
readonly recurringGroupId: Prisma.FieldRef<"CourtBooking", 'String'>
readonly createdAt: Prisma.FieldRef<"CourtBooking", 'DateTime'>
readonly updatedAt: Prisma.FieldRef<"CourtBooking", 'DateTime'>
}
@@ -1582,6 +1785,25 @@ export type CourtBookingDeleteManyArgs<ExtArgs extends runtime.Types.Extensions.
limit?: number
}
/**
* CourtBooking.recurringGroup
*/
export type CourtBooking$recurringGroupArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
/**
* Select specific fields to fetch from the RecurringBookingGroup
*/
select?: Prisma.RecurringBookingGroupSelect<ExtArgs> | null
/**
* Omit specific fields from the RecurringBookingGroup
*/
omit?: Prisma.RecurringBookingGroupOmit<ExtArgs> | null
/**
* Choose, which related nodes to fetch as well
*/
include?: Prisma.RecurringBookingGroupInclude<ExtArgs> | null
where?: Prisma.RecurringBookingGroupWhereInput
}
/**
* CourtBooking without action
*/

View File

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

View File

@@ -22,10 +22,26 @@ export const auth = betterAuth({
},
user: {
additionalFields: {
role: {
type: 'string',
required: false,
},
phone: {
type: 'string',
required: false,
},
banned: {
type: 'boolean',
required: false,
},
bannedAt: {
type: 'string',
required: false,
},
banReason: {
type: 'string',
required: false,
},
},
},
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 { fetchGeoInfo } from '@/lib/geoip';
import { db } from '@/lib/prisma';
import type { AppEnv } from '@/types/hono';
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) => {
const session = await auth.api.getSession({
headers: c.req.raw.headers,
@@ -11,8 +24,45 @@ export const requireAuth = createMiddleware<AppEnv>(async (c, next) => {
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('session', session.session);
if (s.ipAddress && !s.country) {
resolveSessionGeo(s.id, s.ipAddress);
}
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 { cancelRecurringGroupHandler } from '@/modules/admin-booking/handlers/cancel-recurring-group.handler';
import { createAdminBookingHandler } from '@/modules/admin-booking/handlers/create-admin-booking.handler';
import { createAdminRecurringBookingHandler } from '@/modules/admin-booking/handlers/create-admin-recurring-booking.handler';
import { listAdminBookingsHandler } from '@/modules/admin-booking/handlers/list-admin-bookings.handler';
import { listRecurringGroupsHandler } from '@/modules/admin-booking/handlers/list-recurring-groups.handler';
import { updateAdminBookingStatusHandler } from '@/modules/admin-booking/handlers/update-admin-booking-status.handler';
import { updateRecurringGroupHandler } from '@/modules/admin-booking/handlers/update-recurring-group.handler';
import type { AppEnv } from '@/types/hono';
import { zValidator } from '@hono/zod-validator';
import {
createAdminBookingSchema,
createRecurringBookingSchema,
listAdminBookingsQuerySchema,
updateAdminBookingStatusSchema,
updateRecurringGroupSchema,
} from '@repo/api-contract';
import { Hono } from 'hono';
import { z } from 'zod';
@@ -15,6 +21,7 @@ import { z } from 'zod';
export const adminBookingRoutes = new Hono<AppEnv>();
const complexIdParamsSchema = z.object({ complexId: z.uuid() });
const bookingIdParamsSchema = z.object({ id: z.uuid() });
const groupIdParamsSchema = z.object({ groupId: z.uuid() });
adminBookingRoutes.use('*', requireAuth);
@@ -32,6 +39,32 @@ adminBookingRoutes.post(
createAdminBookingHandler
);
adminBookingRoutes.post(
'/complex/:complexId/recurring',
zValidator('param', complexIdParamsSchema),
zValidator('json', createRecurringBookingSchema),
createAdminRecurringBookingHandler
);
adminBookingRoutes.post(
'/recurring/:groupId/cancel',
zValidator('param', groupIdParamsSchema),
cancelRecurringGroupHandler
);
adminBookingRoutes.get(
'/complex/:complexId/recurring',
zValidator('param', complexIdParamsSchema),
listRecurringGroupsHandler
);
adminBookingRoutes.patch(
'/recurring/:groupId',
zValidator('param', groupIdParamsSchema),
zValidator('json', updateRecurringGroupSchema),
updateRecurringGroupHandler
);
adminBookingRoutes.patch(
'/:id/status',
zValidator('param', bookingIdParamsSchema),

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;
customerEmail: string;
status: 'CONFIRMED' | 'CANCELLED' | 'COMPLETED' | 'NOSHOW';
recurringGroupId: string | null;
createdAt: Date;
updatedAt: Date;
court: {
@@ -239,6 +240,7 @@ function mapBookingResponse(booking: {
customerEmail: booking.customerEmail,
price: booking.price ?? 0,
status: booking.status,
recurringGroupId: booking.recurringGroupId,
createdAt: booking.createdAt.toISOString(),
updatedAt: booking.updatedAt.toISOString(),
};
@@ -431,6 +433,7 @@ export async function createAdminBooking(
customerPhone: input.customerPhone.trim(),
customerEmail: input.customerEmail?.trim() ?? '',
status: 'CONFIRMED',
recurringGroupId: null,
},
include: {
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 { db } from '@/lib/prisma';
import { parsePlanRules } from '@/modules/plan/services/plan-rules.service';
import { v7 as uuidv7 } from 'uuid';
export type CreateComplexInput = {
@@ -185,7 +186,11 @@ export async function listMyComplexes(userId: string) {
const complexUsers = await db.complexUser.findMany({
where: { userId },
include: {
complex: true,
complex: {
include: {
plan: true,
},
},
},
orderBy: {
createdAt: 'asc',
@@ -195,6 +200,9 @@ export async function listMyComplexes(userId: string) {
return complexUsers.map((complexUser) => ({
...complexUser.complex,
role: complexUser.role,
planFeatures: complexUser.complex.plan
? parsePlanRules(complexUser.complex.plan.rules).features
: null,
}));
}
@@ -207,7 +215,11 @@ export async function getCurrentComplex(userId: string, complexId: string) {
},
},
include: {
complex: true,
complex: {
include: {
plan: true,
},
},
},
});
@@ -216,6 +228,9 @@ export async function getCurrentComplex(userId: string, complexId: string) {
return {
...complexUser.complex,
role: complexUser.role,
planFeatures: complexUser.complex.plan
? parsePlanRules(complexUser.complex.plan.rules).features
: null,
};
}
@@ -228,7 +243,11 @@ export async function selectComplex(userId: string, complexId: string) {
},
},
include: {
complex: true,
complex: {
include: {
plan: true,
},
},
},
});
@@ -237,6 +256,9 @@ export async function selectComplex(userId: string, complexId: string) {
return {
...complexUser.complex,
role: complexUser.role,
planFeatures: complexUser.complex.plan
? parsePlanRules(complexUser.complex.plan.rules).features
: null,
};
}

View File

@@ -1,5 +1,6 @@
import { createHash, randomInt } from 'node:crypto';
import { auth } from '@/lib/auth';
import { type IpGeoInfo, fetchGeoInfo } from '@/lib/geoip';
import { sendMail } from '@/lib/mailer';
import { db } from '@/lib/prisma';
import type {
@@ -82,13 +83,6 @@ type UserAgentInfo = {
device: string;
};
type IpGeoInfo = {
ip: string;
city: string;
country: string;
countryCode: string;
};
function parseUserAgent(ua: string | undefined): UserAgentInfo {
if (!ua) {
return { browser: 'Desconocido', os: 'Desconocido', device: 'Desconocido' };
@@ -118,31 +112,6 @@ function parseUserAgent(ua: string | undefined): UserAgentInfo {
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) {
await sendMail({
to: email,

View File

@@ -1,4 +1,6 @@
import { fetchGeoInfo } from '@/lib/geoip';
import { db } from '@/lib/prisma';
import { resolvePlanPrice } from '@/modules/plan/services/plan-pricing.service';
import { parsePlanRules } from '@/modules/plan/services/plan-rules.service';
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(
plans.map((plan) => {
const rules = parsePlanRules(plan.rules);
const resolved = resolvePlanPrice(Number(plan.price), rules, countryCode);
return {
code: plan.code,
name: plan.name,
price: Number(plan.price),
price: resolved.amount,
currency: resolved.currency,
features: rules.features,
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()) {
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);

View File

@@ -1,5 +1,6 @@
import path from 'node:path';
import { adminBookingRoutes } from '@/modules/admin-booking/admin-booking.routes';
import { adminRoutes } from '@/modules/admin/admin.routes';
import { authRoutes } from '@/modules/auth/auth.routes';
import { complexRoutes } from '@/modules/complex/complex.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>) {
app
.route('', authRoutes)
.route('/api/admin', adminRoutes)
.route('/api/user', userRoutes)
.route('/api/plans', planRoutes)
.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,
Moon,
Settings,
Shield,
Sun,
User,
X,
@@ -241,6 +242,18 @@ export function Header() {
</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 />
<DropdownMenuItem
@@ -405,6 +418,20 @@ export function Header() {
</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 && (
<Button
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 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 {
type ReactNode,
@@ -44,6 +50,16 @@ interface CreateManualBookingPayload {
customerEmail: string;
}
interface CreateRecurringBookingPayload {
courtId: string;
date: string;
startTime: string;
customerName: string;
customerPhone: string;
customerEmail: string;
recurringEndDate?: string;
}
interface BookingContextValue {
complex: ComplexWithRole;
courts: Court[];
@@ -66,6 +82,8 @@ interface BookingContextValue {
createBookingError: string | null;
isCreatingBooking: boolean;
bookingToolsOpen: boolean;
planFeatures: PlanFeatureFlags | null;
isRecurringEnabled: boolean;
setSelectedDate: (date: string) => void;
moveSelectedDate: (amount: number) => void;
setSelectedSportId: (sportId: string) => void;
@@ -73,8 +91,14 @@ interface BookingContextValue {
setViewMode: (mode: BookingViewMode) => void;
setVisibleTimeRange: (range: BookingTimeRange) => void;
openCreateBooking: (draft?: Partial<BookingSlotDraft>) => void;
recurringGroups: RecurringBookingGroup[];
isRecurringGroupsLoading: boolean;
cancelRecurringGroup: (groupId: string) => void;
isCancellingRecurringGroup: boolean;
refreshRecurringGroups: () => void;
closeCreateBooking: () => void;
createBooking: (payload: CreateManualBookingPayload) => Promise<void>;
createRecurringBooking: (payload: CreateRecurringBookingPayload) => Promise<void>;
updateBookingStatus: (bookingId: string, status: 'CANCELLED' | 'COMPLETED' | 'NOSHOW') => void;
exportDayReport: () => void;
openBookingTools: (segment?: BookingTimelineSegment) => void;
@@ -147,6 +171,9 @@ function buildSegmentsForCourt(
const segments: BookingTimelineSegment[] = [];
const slotDuration = court.slotDurationMinutes;
const nowMinutes = timeToMinutes(getNowTime());
const isToday = isTodayIso(selectedDate);
const availabilityRanges = court.availability
.filter((range) => range.dayOfWeek === dayOfWeek)
.map((range) => ({
@@ -181,6 +208,9 @@ function buildSegmentsForCourt(
slotStart += slotDuration
) {
const slotEnd = slotStart + slotDuration;
if (isToday && slotEnd <= nowMinutes) continue;
const isBooked = courtBookings.some((booking) =>
overlapsBooking(slotStart, slotEnd, booking)
);
@@ -239,6 +269,20 @@ export function BookingProvider({ children, complex }: BookingProviderProps) {
queryFn: () => apiClient.courts.listByComplex(complex.id),
});
const recurringGroupsQuery = useQuery({
queryKey: ['recurring-groups', complex.id],
queryFn: () => apiClient.adminBookings.listRecurringGroups(complex.id),
enabled: viewMode === 'recurring',
});
const cancelRecurringGroupMutation = useMutation({
mutationFn: (groupId: string) => apiClient.adminBookings.cancelRecurringGroup(groupId),
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: ['recurring-groups', complex.id] });
await queryClient.invalidateQueries({ queryKey: ['admin-bookings', complex.id] });
},
});
const bookingsQuery = useQuery({
queryKey: ['admin-bookings', complex.id, selectedDate],
queryFn: () =>
@@ -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({
mutationFn: (payload: { bookingId: string; status: 'CANCELLED' | 'COMPLETED' | 'NOSHOW' }) =>
apiClient.adminBookings.updateStatus(payload.bookingId, { status: payload.status }),
@@ -407,6 +471,16 @@ export function BookingProvider({ children, complex }: BookingProviderProps) {
URL.revokeObjectURL(url);
}, [bookings, complex.complexSlug, selectedDate]);
const planFeatures = complex.planFeatures ?? null;
const isRecurringEnabled = planFeatures?.fixedSlots ?? false;
const createRecurringBooking = useCallback(
async (payload: CreateRecurringBookingPayload) => {
await createRecurringBookingMutation.mutateAsync(payload);
},
[createRecurringBookingMutation]
);
const value = useMemo<BookingContextValue>(
() => ({
complex,
@@ -431,11 +505,24 @@ export function BookingProvider({ children, complex }: BookingProviderProps) {
'No pudimos cargar el panel de reservas.'
)
: null,
recurringGroups: recurringGroupsQuery.data?.groups ?? [],
isRecurringGroupsLoading: recurringGroupsQuery.isLoading,
cancelRecurringGroup: (groupId: string) => cancelRecurringGroupMutation.mutate(groupId),
isCancellingRecurringGroup: cancelRecurringGroupMutation.isPending,
refreshRecurringGroups: () =>
queryClient.invalidateQueries({ queryKey: ['recurring-groups', complex.id] }),
isCreateBookingOpen,
createBookingError: createBookingMutation.isError
? extractMessage(createBookingMutation.error, 'No pudimos crear la reserva.')
: null,
isCreatingBooking: createBookingMutation.isPending,
createBookingError:
createBookingMutation.isError || createRecurringBookingMutation.isError
? extractMessage(
createBookingMutation.error ?? createRecurringBookingMutation.error,
'No pudimos crear la reserva.'
)
: null,
isCreatingBooking:
createBookingMutation.isPending || createRecurringBookingMutation.isPending,
planFeatures,
isRecurringEnabled,
setSelectedDate,
moveSelectedDate: (amount) => setSelectedDate((date) => addDaysIso(date, amount)),
setSelectedSportId,
@@ -445,6 +532,7 @@ export function BookingProvider({ children, complex }: BookingProviderProps) {
openCreateBooking,
closeCreateBooking,
createBooking,
createRecurringBooking,
updateBookingStatus,
exportDayReport,
bookingToolsOpen,
@@ -467,10 +555,16 @@ export function BookingProvider({ children, complex }: BookingProviderProps) {
createBookingMutation.error,
createBookingMutation.isError,
createBookingMutation.isPending,
createRecurringBooking,
createRecurringBookingMutation.error,
createRecurringBookingMutation.isError,
createRecurringBookingMutation.isPending,
currentTime,
exportDayReport,
isCreateBookingOpen,
openCreateBooking,
planFeatures,
isRecurringEnabled,
schedules,
selectedDate,
selectedSlot,
@@ -482,6 +576,9 @@ export function BookingProvider({ children, complex }: BookingProviderProps) {
viewMode,
visibleTimeRange,
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 type { ComplexWithRole } from '@repo/api-contract';
import { useState } from 'react';
import { BookingProvider } from './booking-provider';
import { BookingProvider, useBooking } from './booking-provider';
import { BookingCreateDialog } from './components/booking-create-dialog';
import { BookingDaySummary, BookingQuickActions } from './components/booking-day-summary';
import { BookingHeader } from './components/booking-header';
import { BookingListView } from './components/booking-list-view';
import { BookingMobile } from './components/booking-mobile';
import { BookingTimeline } from './components/booking-timeline';
import { BookingToolbar } from './components/booking-toolbar';
import { BookingToolsDialog } from './components/booking-tools-dialog';
import { RecurringBookingListView } from './components/recurring-booking-list';
interface BookingProps {
complex: ComplexWithRole;
}
export function Booking({ complex }: BookingProps) {
const isMobile = useIsMobile();
const { user } = useAuth();
const [sending, setSending] = useState(false);
const [emailSent, setEmailSent] = useState(false);
@@ -56,21 +57,38 @@ export function Booking({ complex }: BookingProps) {
</button>
</div>
)}
{isMobile ? (
<BookingMobile />
) : (
<div className="flex flex-col gap-5">
<BookingHeader />
<BookingToolbar />
<BookingTimeline />
<div className="grid gap-5 xl:grid-cols-4">
<BookingDaySummary />
<BookingQuickActions />
</div>
</div>
)}
<BookingInner />
<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">
<BookingHeader />
<BookingToolbar />
<BookingTimeline />
<div className="grid gap-5 xl:grid-cols-4">
<BookingDaySummary />
<BookingQuickActions />
</div>
</div>
);
}

View File

@@ -1,6 +1,6 @@
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 BookingSegmentStatus = 'free' | 'reserved';

View File

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

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

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',
advancedReports: 'Reportes avanzados',
whatsappReminders: 'Recordatorios WhatsApp',
fixedSlots: 'Turnos fijos',
};
export function PlanSelectStep({ form, plans }: PlanSelectStepProps) {
@@ -24,6 +25,7 @@ export function PlanSelectStep({ form, plans }: PlanSelectStepProps) {
'onlinePayments',
'advancedReports',
'whatsappReminders',
'fixedSlots',
] as const;
return (
@@ -58,7 +60,11 @@ export function PlanSelectStep({ form, plans }: PlanSelectStepProps) {
<div className="mb-3">
<h3 className="text-xl font-bold">{plan.name}</h3>
<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>
</div>
</div>

View File

@@ -50,7 +50,14 @@ export function ReviewStep({ data, selectedPlan }: ReviewStepProps) {
<Section title="Plan seleccionado">
{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">
{[
['publicBookingPage', 'Página de booking pública'],

View File

@@ -26,7 +26,6 @@ import {
Check,
ChevronLeft,
ChevronRight,
Clock3,
Dumbbell,
Lock,
MapPin,
@@ -34,7 +33,6 @@ import {
Moon,
Share2,
ShieldCheck,
Shirt,
Sparkles,
Sun,
Trophy,
@@ -414,7 +412,7 @@ function PublicBookingPageChrome({
if (mobile) {
return (
<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">
<PlayzerBrand compact />
<PublicBookingThemeToggle />
@@ -441,8 +439,6 @@ function PublicBookingPageChrome({
</div>
{children}
<MobileBottomNav />
</div>
</main>
);
@@ -553,8 +549,7 @@ function PublicBookingDesktop(props: BookingShellProps) {
const currentStep = getStep(selectedSlot, props.form.formState.isValid);
const completedSteps = getCompletedSteps(selectedSlot, props.form.formState.isValid);
const selectedDay = dayOptions.find((option) => option.value === selectedDate);
const currentDayIndex = dayOptions.findIndex((option) => option.value === selectedDate);
const canGoBackButton = canGoBack || currentDayIndex > 0;
const canGoBackButton = canGoBack;
return (
<PublicBookingPageChrome
@@ -683,10 +678,7 @@ function PublicBookingDesktop(props: BookingShellProps) {
</div>
</Panel>
<div className="grid grid-cols-[minmax(0,1fr)_minmax(340px,0.95fr)] gap-4">
<CourtDetails court={selectedCourt} />
<SelectedSlotCard {...props} />
</div>
<SelectedSlotCard {...props} />
</>
)}
</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 }) {
const {
selectedSlot,
@@ -1217,9 +1174,11 @@ function SelectedSlotCard(props: BookingShellProps & { compact?: boolean }) {
useEffect(() => {
if (!selectedSlot) return;
window.requestAnimationFrame(() => {
customerNameInputRef.current?.focus();
});
const input = customerNameInputRef.current;
if (!input) return;
input.focus();
input.scrollIntoView({ behavior: 'smooth', block: 'center' });
}, [selectedSlot]);
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) {
const navigate = useNavigate();
const isMobile = useIsMobile();
@@ -1579,7 +1504,8 @@ export function PublicBookingPage({ complexSlug }: PublicBookingPageProps) {
visibleCourts.length,
]);
const canGoBack = windowStartOffset > 0;
const currentDayIndex = dayOptions.findIndex((option) => option.value === selectedDate);
const canGoBack = windowStartOffset > 0 || currentDayIndex > 0;
const selectDate = (date: string) => {
setSelectedDate(date);

View File

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

View File

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

View File

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

View File

@@ -6,6 +6,7 @@ export * as courts from './resources/courts';
export * as user from './resources/user';
export * as sports from './resources/sports';
export * as plans from './resources/plans';
export * as admin from './resources/admin';
export {
cancelPublic,
@@ -14,5 +15,9 @@ export {
getConfirmation,
listByComplex,
createAdmin,
createRecurring,
cancelRecurringGroup,
listRecurringGroups,
updateRecurringGroup,
updateStatus,
} 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,
CreateAdminBookingInput,
CreatePublicBookingInput,
ListRecurringGroupsResponse,
PublicAvailabilityResponse,
PublicBooking,
PublicBookingConfirmation,
RecurringBookingGroup,
UpdateAdminBookingStatusInput,
UpdateRecurringGroupInput,
} from '@repo/api-contract';
import type { CreateRecurringBookingInput } from '@repo/api-contract';
import { http } from '../http';
export async function getAvailability(
@@ -52,6 +56,36 @@ export async function createAdmin(complexId: string, payload: CreateAdminBooking
return response.data;
}
export async function createRecurring(complexId: string, payload: CreateRecurringBookingInput) {
const response = await http.post<RecurringBookingGroup>(
`/api/admin-bookings/complex/${complexId}/recurring`,
payload
);
return response.data;
}
export async function cancelRecurringGroup(groupId: string) {
const response = await http.post<{ ok: boolean }>(
`/api/admin-bookings/recurring/${groupId}/cancel`
);
return response.data;
}
export async function listRecurringGroups(complexId: string) {
const response = await http.get<ListRecurringGroupsResponse>(
`/api/admin-bookings/complex/${complexId}/recurring`
);
return response.data;
}
export async function updateRecurringGroup(groupId: string, payload: UpdateRecurringGroupInput) {
const response = await http.patch<RecurringBookingGroup>(
`/api/admin-bookings/recurring/${groupId}`,
payload
);
return response.data;
}
export async function cancelPublic(complexSlug: string, payload: CancelPublicBookingInput) {
const response = await http.post<PublicBooking>(
`/api/public-bookings/complex/${complexSlug}/cancel`,

View File

@@ -27,6 +27,14 @@ function AppRouter() {
useEffect(() => {
configureApiClient({
onUnauthorized: async () => {
if (!auth.isAuthenticated) {
return;
}
await auth.signOut();
await router.navigate({ to: '/login' });
},
onForbidden: async (error) => {
if (error.message?.includes('verificar tu email')) {
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 AppAuthenticatedIndexRouteImport } from './routes/_app/_authenticated/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 AppAuthenticatedComplexSlugEditRouteImport } from './routes/_app/_authenticated/complex/$slug/edit'
@@ -94,6 +99,36 @@ const ComplexSlugBookingIndexRoute = ComplexSlugBookingIndexRouteImport.update({
path: '/',
getParentRoute: () => ComplexSlugBookingRoute,
} 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 =
ComplexSlugBookingConfirmedBookingCodeRouteImport.update({
id: '/confirmed/$bookingCode',
@@ -119,8 +154,13 @@ export interface FileRoutesByFullPath {
'/$complexSlug/booking': typeof ComplexSlugBookingRouteWithChildren
'/profile': typeof AppProfileRoute
'/onboard/setup': typeof OnboardSetupRoute
'/admin': typeof AppAuthenticatedAdminRouteRouteWithChildren
'/$complexSlug/booking/': typeof ComplexSlugBookingIndexRoute
'/$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
}
export interface FileRoutesByTo {
@@ -136,6 +176,10 @@ export interface FileRoutesByTo {
'/onboard/setup': typeof OnboardSetupRoute
'/$complexSlug/booking': typeof ComplexSlugBookingIndexRoute
'/$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
}
export interface FileRoutesById {
@@ -152,9 +196,14 @@ export interface FileRoutesById {
'/$complexSlug/booking': typeof ComplexSlugBookingRouteWithChildren
'/_app/profile': typeof AppProfileRoute
'/onboard/setup': typeof OnboardSetupRoute
'/_app/_authenticated/admin': typeof AppAuthenticatedAdminRouteRouteWithChildren
'/$complexSlug/booking/': typeof ComplexSlugBookingIndexRoute
'/_app/_authenticated/': typeof AppAuthenticatedIndexRoute
'/$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
}
export interface FileRouteTypes {
@@ -171,8 +220,13 @@ export interface FileRouteTypes {
| '/$complexSlug/booking'
| '/profile'
| '/onboard/setup'
| '/admin'
| '/$complexSlug/booking/'
| '/$complexSlug/booking/confirmed/$bookingCode'
| '/admin/complexes'
| '/admin/plans'
| '/admin/users'
| '/admin/'
| '/complex/$slug/edit'
fileRoutesByTo: FileRoutesByTo
to:
@@ -188,6 +242,10 @@ export interface FileRouteTypes {
| '/onboard/setup'
| '/$complexSlug/booking'
| '/$complexSlug/booking/confirmed/$bookingCode'
| '/admin/complexes'
| '/admin/plans'
| '/admin/users'
| '/admin'
| '/complex/$slug/edit'
id:
| '__root__'
@@ -203,9 +261,14 @@ export interface FileRouteTypes {
| '/$complexSlug/booking'
| '/_app/profile'
| '/onboard/setup'
| '/_app/_authenticated/admin'
| '/$complexSlug/booking/'
| '/_app/_authenticated/'
| '/$complexSlug/booking/confirmed/$bookingCode'
| '/_app/_authenticated/admin/complexes'
| '/_app/_authenticated/admin/plans'
| '/_app/_authenticated/admin/users'
| '/_app/_authenticated/admin/'
| '/_app/_authenticated/complex/$slug/edit'
fileRoutesById: FileRoutesById
}
@@ -321,6 +384,41 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof ComplexSlugBookingIndexRouteImport
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': {
id: '/$complexSlug/booking/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 {
AppAuthenticatedAdminRouteRoute: typeof AppAuthenticatedAdminRouteRouteWithChildren
AppAuthenticatedIndexRoute: typeof AppAuthenticatedIndexRoute
AppAuthenticatedComplexSlugEditRoute: typeof AppAuthenticatedComplexSlugEditRoute
}
const AppAuthenticatedRouteRouteChildren: AppAuthenticatedRouteRouteChildren = {
AppAuthenticatedAdminRouteRoute: AppAuthenticatedAdminRouteRouteWithChildren,
AppAuthenticatedIndexRoute: AppAuthenticatedIndexRoute,
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]
exclude = ["**/dist/**", "**/node_modules/**"]
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 ISO_DATE_REGEX = /^\d{4}-\d{2}-\d{2}$/
const BOOKING_CODE_REGEX = /^[A-Z0-9]{6,8}$/
const TIME_REGEX = /^([01]\d|2[0-3]):([0-5]\d)$/;
const ISO_DATE_REGEX = /^\d{4}-\d{2}-\d{2}$/;
const BOOKING_CODE_REGEX = /^[A-Z0-9]{6,8}$/;
export const bookingStatusSchema = z.enum(['CONFIRMED', 'CANCELLED', 'COMPLETED', 'NOSHOW'])
export const bookingStatusSchema = z.enum(['CONFIRMED', 'CANCELLED', 'COMPLETED', 'NOSHOW']);
export const recurringGroupStatusSchema = z.enum(['ACTIVE', 'CANCELLED']);
export const adminBookingSportSchema = z.object({
id: z.uuid(),
name: z.string(),
slug: z.string(),
})
});
export const adminBookingSchema = z.object({
id: z.uuid(),
@@ -28,17 +30,18 @@ export const adminBookingSchema = z.object({
customerEmail: z.string(),
price: z.number().nonnegative(),
status: bookingStatusSchema,
recurringGroupId: z.string().uuid().nullable().optional(),
createdAt: z.string().datetime(),
updatedAt: z.string().datetime(),
})
});
export const listAdminBookingsQuerySchema = z.object({
fromDate: z.string().regex(ISO_DATE_REGEX, 'La fecha debe tener formato YYYY-MM-DD.'),
})
});
export const listAdminBookingsResponseSchema = z.object({
bookings: z.array(adminBookingSchema),
})
});
export const createAdminBookingSchema = z.object({
date: z.string().regex(ISO_DATE_REGEX, 'La fecha debe tener formato YYYY-MM-DD.'),
@@ -54,20 +57,73 @@ export const createAdminBookingSchema = z.object({
.trim()
.min(6, 'El telefono debe tener al menos 6 caracteres.')
.max(30, 'El telefono no puede superar los 30 caracteres.'),
customerEmail: z
.string()
.email('El email ingresado no es valido.')
.or(z.literal('')),
})
customerEmail: z.string().email('El email ingresado no es valido.').or(z.literal('')),
});
export const updateAdminBookingStatusSchema = z.object({
status: z.enum(['CANCELLED', 'COMPLETED', 'NOSHOW']),
})
});
export type BookingStatus = z.infer<typeof bookingStatusSchema>
export type AdminBookingSport = z.infer<typeof adminBookingSportSchema>
export type AdminBooking = z.infer<typeof adminBookingSchema>
export type ListAdminBookingsQuery = z.infer<typeof listAdminBookingsQuerySchema>
export type ListAdminBookingsResponse = z.infer<typeof listAdminBookingsResponseSchema>
export type CreateAdminBookingInput = z.infer<typeof createAdminBookingSchema>
export type UpdateAdminBookingStatusInput = z.infer<typeof updateAdminBookingStatusSchema>
export const createRecurringBookingSchema = createAdminBookingSchema.extend({
isRecurring: z.literal(true),
recurringEndDate: z
.string()
.regex(ISO_DATE_REGEX, 'La fecha debe tener formato YYYY-MM-DD.')
.optional(),
});
export const recurringBookingGroupSchema = z.object({
id: z.uuid(),
complexId: z.uuid(),
courtId: z.uuid(),
startTime: z.string().regex(TIME_REGEX),
endTime: z.string().regex(TIME_REGEX),
dayOfWeek: z.enum(['MONDAY', 'TUESDAY', 'WEDNESDAY', 'THURSDAY', 'FRIDAY', 'SATURDAY', 'SUNDAY']),
startDate: z.string().regex(ISO_DATE_REGEX),
endDate: z.string().regex(ISO_DATE_REGEX).nullable(),
status: recurringGroupStatusSchema,
customerName: z.string(),
customerPhone: z.string(),
customerEmail: z.string(),
bookings: z.array(adminBookingSchema),
createdAt: z.string().datetime(),
updatedAt: z.string().datetime(),
});
export const 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([
'MONDAY',
@@ -8,7 +9,7 @@ const dayOfWeekEnum = z.enum([
'FRIDAY',
'SATURDAY',
'SUNDAY',
])
]);
export const complexSchema = z.object({
id: z.uuid(),
@@ -22,7 +23,7 @@ export const complexSchema = z.object({
planCode: z.string().max(10).nullable(),
createdAt: z.string().datetime(),
updatedAt: z.string().datetime(),
})
});
export const createComplexSchema = z
.object({
@@ -36,21 +37,13 @@ export const createComplexSchema = z
.trim()
.min(5, 'La direccion debe tener al menos 5 caracteres.')
.max(200, 'La direccion no puede superar los 200 caracteres.'),
city: z
.string()
.trim()
.max(100, 'La ciudad no puede superar los 100 caracteres.')
.optional(),
city: z.string().trim().max(100, 'La ciudad no puede superar los 100 caracteres.').optional(),
state: z
.string()
.trim()
.max(100, 'La provincia/estado no puede superar los 100 caracteres.')
.optional(),
country: z
.string()
.trim()
.max(100, 'El país no puede superar los 100 caracteres.')
.optional(),
country: z.string().trim().max(100, 'El país no puede superar los 100 caracteres.').optional(),
planCode: z
.string()
.trim()
@@ -70,31 +63,31 @@ export const createComplexSchema = z
code: z.ZodIssueCode.custom,
path: ['courtSportId'],
message: 'Debes seleccionar un deporte.',
})
});
}
if (!data.courtStartTime || !/^\d{2}:\d{2}$/.test(data.courtStartTime)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ['courtStartTime'],
message: 'La hora de inicio debe tener formato HH:mm.',
})
});
}
if (!data.courtEndTime || !/^\d{2}:\d{2}$/.test(data.courtEndTime)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ['courtEndTime'],
message: 'La hora de fin debe tener formato HH:mm.',
})
});
}
if (!data.courtDaysOfWeek || data.courtDaysOfWeek.length === 0) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ['courtDaysOfWeek'],
message: 'Debes seleccionar al menos un día.',
})
});
}
}
})
});
export const updateComplexSchema = z
.object({
@@ -154,16 +147,16 @@ export const updateComplexSchema = z
payload.country !== undefined ||
payload.complexSlug ||
payload.adminEmail ||
payload.planCode !== undefined,
payload.planCode !== undefined
),
{
message: 'Debes enviar al menos un campo para actualizar.',
},
)
}
);
export const complexUserRoleSchema = z.enum(['ADMIN', 'EMPLOYEE'])
export const complexUserRoleSchema = z.enum(['ADMIN', 'EMPLOYEE']);
export type ComplexUserRole = z.infer<typeof complexUserRoleSchema>
export type ComplexUserRole = z.infer<typeof complexUserRoleSchema>;
export const complexMemberSchema = z.object({
userId: z.string(),
@@ -172,13 +165,13 @@ export const complexMemberSchema = z.object({
avatarUrl: z.url(),
role: complexUserRoleSchema,
createdAt: z.string().datetime(),
})
});
export type ComplexMember = z.infer<typeof complexMemberSchema>
export type ComplexMember = z.infer<typeof complexMemberSchema>;
export const complexInvitationStatusSchema = z.enum(['PENDING', 'ACCEPTED', 'REVOKED', 'EXPIRED'])
export const complexInvitationStatusSchema = z.enum(['PENDING', 'ACCEPTED', 'REVOKED', 'EXPIRED']);
export type ComplexInvitationStatus = z.infer<typeof complexInvitationStatusSchema>
export type ComplexInvitationStatus = z.infer<typeof complexInvitationStatusSchema>;
export const complexInvitationSchema = z.object({
id: z.uuid(),
@@ -189,62 +182,63 @@ export const complexInvitationSchema = z.object({
expiresAt: z.string().datetime(),
acceptedAt: z.string().datetime().nullable(),
revokedAt: z.string().datetime().nullable(),
})
});
export type ComplexInvitation = z.infer<typeof complexInvitationSchema>
export type ComplexInvitation = z.infer<typeof complexInvitationSchema>;
export const complexUsersOverviewSchema = z.object({
members: z.array(complexMemberSchema),
invitations: z.array(complexInvitationSchema),
})
});
export type ComplexUsersOverview = z.infer<typeof complexUsersOverviewSchema>
export type ComplexUsersOverview = z.infer<typeof complexUsersOverviewSchema>;
export const inviteComplexUserSchema = z.object({
email: z.email('El email debe ser válido.'),
})
});
export type InviteComplexUserInput = z.infer<typeof inviteComplexUserSchema>
export type InviteComplexUserInput = z.infer<typeof inviteComplexUserSchema>;
export const acceptComplexInvitationsSchema = z.object({
inviteToken: z.string().min(1, 'El token de invitación no puede estar vacío.').optional(),
})
});
export type AcceptComplexInvitationsInput = z.infer<typeof acceptComplexInvitationsSchema>
export type AcceptComplexInvitationsInput = z.infer<typeof acceptComplexInvitationsSchema>;
export const revokeComplexUserSchema = z.object({
userId: z.string().min(1, 'El userId no puede estar vacío.'),
})
});
export type RevokeComplexUserInput = z.infer<typeof revokeComplexUserSchema>
export type RevokeComplexUserInput = z.infer<typeof revokeComplexUserSchema>;
export const selectComplexSchema = z.object({
complexId: z.string().uuid(),
})
});
export type SelectComplexInput = z.infer<typeof selectComplexSchema>
export type SelectComplexInput = z.infer<typeof selectComplexSchema>;
export const complexWithRoleSchema = complexSchema.merge(
z.object({
role: complexUserRoleSchema,
planFeatures: planFeatureFlagsSchema.nullable(),
})
)
);
export type ComplexWithRole = z.infer<typeof complexWithRoleSchema>
export type ComplexWithRole = z.infer<typeof complexWithRoleSchema>;
export type Complex = z.infer<typeof complexSchema>
export type CreateComplexInput = z.infer<typeof createComplexSchema>
export type UpdateComplexInput = z.infer<typeof updateComplexSchema>
export type CreateComplexPayload = CreateComplexInput
export type CreateComplexResponse = Complex
export type UpdateComplexPayload = UpdateComplexInput
export type UpdateComplexResponse = Complex
export type Complex = z.infer<typeof complexSchema>;
export type CreateComplexInput = z.infer<typeof createComplexSchema>;
export type UpdateComplexInput = z.infer<typeof updateComplexSchema>;
export type CreateComplexPayload = CreateComplexInput;
export type CreateComplexResponse = Complex;
export type UpdateComplexPayload = UpdateComplexInput;
export type UpdateComplexResponse = Complex;
export type InviteComplexUserResponse = {
invitation: ComplexInvitation
}
invitation: ComplexInvitation;
};
export type AcceptComplexInvitationsResponse = {
acceptedCount: number
}
acceptedCount: number;
};
export type CancelComplexInvitationResponse = {
ok: boolean
}
ok: boolean;
};

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

View File

@@ -6,7 +6,7 @@ export {
planSummarySchema,
planWithFeaturesSchema,
planLimitsSchema,
} from './plan'
} from './plan';
export type {
Plan,
PlanFeatureFlags,
@@ -15,7 +15,7 @@ export type {
PlanSummary,
PlanWithFeatures,
PlanLimits,
} from './plan'
} from './plan';
export {
acceptComplexInvitationsSchema,
complexInvitationSchema,
@@ -30,7 +30,7 @@ export {
revokeComplexUserSchema,
selectComplexSchema,
updateComplexSchema,
} from './complex'
} from './complex';
export type {
AcceptComplexInvitationsInput,
AcceptComplexInvitationsResponse,
@@ -52,7 +52,7 @@ export type {
UpdateComplexPayload,
UpdateComplexResponse,
RevokeComplexUserInput,
} from './complex'
} from './complex';
export {
dayOfWeekSchema,
courtAvailabilityInputSchema,
@@ -61,7 +61,7 @@ export {
courtSchema,
createCourtSchema,
updateCourtSchema,
} from './court'
} from './court';
export type {
DayOfWeek,
Court,
@@ -70,25 +70,35 @@ export type {
CourtPriceRule,
CreateCourtInput,
UpdateCourtInput,
} from './court'
} from './court';
export {
adminBookingSchema,
adminBookingSportSchema,
bookingStatusSchema,
createAdminBookingSchema,
createRecurringBookingSchema,
listAdminBookingsQuerySchema,
listAdminBookingsResponseSchema,
listRecurringGroupsResponseSchema,
recurringBookingGroupSchema,
recurringGroupStatusSchema,
updateAdminBookingStatusSchema,
} from './admin-booking'
updateRecurringGroupSchema,
} from './admin-booking';
export type {
AdminBooking,
AdminBookingSport,
BookingStatus,
CreateAdminBookingInput,
CreateRecurringBookingInput,
ListAdminBookingsQuery,
ListAdminBookingsResponse,
ListRecurringGroupsResponse,
RecurringBookingGroup,
RecurringGroupStatus,
UpdateAdminBookingStatusInput,
} from './admin-booking'
UpdateRecurringGroupInput,
} from './admin-booking';
export {
createPublicBookingSchema,
publicAvailabilityCourtSchema,
@@ -99,7 +109,7 @@ export {
publicBookingSlotSchema,
publicBookingSportSchema,
cancelPublicBookingSchema,
} from './public-booking'
} from './public-booking';
export type {
CreatePublicBookingInput,
CancelPublicBookingInput,
@@ -110,15 +120,15 @@ export type {
PublicBookingConfirmation,
PublicBookingSlot,
PublicBookingSport,
} from './public-booking'
export { sportSchema, createSportSchema, updateSportSchema } from './sport'
export type { Sport, CreateSportInput, UpdateSportInput } from './sport'
} from './public-booking';
export { sportSchema, createSportSchema, updateSportSchema } from './sport';
export type { Sport, CreateSportInput, UpdateSportInput } from './sport';
export {
userProfileSchema,
passwordResetStartSchema,
passwordResetVerifySchema,
passwordResetResendSchema,
} from './user'
} from './user';
export type {
UserProfile,
UserProfileResponse,
@@ -128,4 +138,32 @@ export type {
PasswordResetStartResponse,
PasswordResetVerifyResponse,
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 positiveInt = z.int().positive()
const nonNegativeInt = z.int().nonnegative();
const positiveInt = z.int().positive();
export const planFeatureFlagsSchema = z.object({
onlinePayments: z.boolean().default(false),
publicBookingPage: z.boolean().default(false),
advancedReports: z.boolean().default(false),
whatsappReminders: z.boolean().default(false),
})
fixedSlots: z.boolean().default(false),
});
export const planRulesV1Schema = z.object({
version: z.literal('v1'),
@@ -17,6 +18,7 @@ export const planRulesV1Schema = z.object({
maxBookingsPerDay: positiveInt,
maxActiveUsers: positiveInt.optional(),
maxConcurrentBookingsPerSlot: positiveInt.optional(),
maxSports: positiveInt.optional(),
}),
policies: z
.object({
@@ -30,10 +32,28 @@ export const planRulesV1Schema = z.object({
publicBookingPage: false,
advancedReports: 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({
code: z.string().trim().min(1).max(10),
@@ -41,33 +61,36 @@ export const planSchema = z.object({
price: z.number().nonnegative(),
lastUpdatedAt: z.string().datetime(),
rules: planRulesSchema,
})
});
export const planSummarySchema = z.object({
code: z.string().trim().min(1).max(10),
name: z.string().trim().min(1).max(30),
price: z.number().nonnegative(),
})
});
export const planLimitsSchema = z.object({
maxCourts: positiveInt,
maxBookingsPerDay: positiveInt,
maxActiveUsers: positiveInt.optional(),
maxConcurrentBookingsPerSlot: positiveInt.optional(),
})
maxSports: positiveInt.optional(),
});
export const planWithFeaturesSchema = z.object({
code: z.string().trim().min(1).max(10),
name: z.string().trim().min(1).max(30),
price: z.number().nonnegative(),
currency: z.string().length(3),
features: planFeatureFlagsSchema,
limits: planLimitsSchema,
})
});
export type PlanFeatureFlags = z.infer<typeof planFeatureFlagsSchema>
export type PlanRulesV1 = z.infer<typeof planRulesV1Schema>
export type PlanRules = z.infer<typeof planRulesSchema>
export type Plan = z.infer<typeof planSchema>
export type PlanSummary = z.infer<typeof planSummarySchema>
export type PlanWithFeatures = z.infer<typeof planWithFeaturesSchema>
export type PlanLimits = z.infer<typeof planLimitsSchema>
export type PlanFeatureFlags = z.infer<typeof planFeatureFlagsSchema>;
export type PlanRulesV1 = z.infer<typeof planRulesV1Schema>;
export type PlanRulesV2 = z.infer<typeof planRulesV2Schema>;
export type PlanRules = z.infer<typeof planRulesSchema>;
export type Plan = z.infer<typeof planSchema>;
export type PlanSummary = z.infer<typeof planSummarySchema>;
export type PlanWithFeatures = z.infer<typeof planWithFeaturesSchema>;
export type PlanLimits = z.infer<typeof planLimitsSchema>;

View File

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

View File

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

View File

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