Compare commits
52 Commits
feat/onboa
...
fd4d8b7abd
| Author | SHA1 | Date | |
|---|---|---|---|
| fd4d8b7abd | |||
|
|
3b3def94ab | ||
|
|
1318e3bf57 | ||
|
|
1b5eb253f2 | ||
| ccee416b6f | |||
|
|
c3e26f8f0e | ||
|
|
ef926c63a2 | ||
|
|
dc8a10a612 | ||
|
|
00f0cf511f | ||
|
|
93b3a82638 | ||
|
|
c1c2f18471 | ||
|
|
c278c78e3d | ||
|
|
d767ac3ac7 | ||
|
|
8c3ca0e7e1 | ||
|
|
72f273354e | ||
|
|
ef2ded9d64 | ||
|
|
4c7b825129 | ||
| 339e6a70d7 | |||
|
|
c70541b4f5 | ||
| 89673c3844 | |||
|
|
630dedb507 | ||
|
|
49d2a13672 | ||
|
|
b2f9a14b87 | ||
|
|
b48dd4f15d | ||
|
|
fee0be7e1f | ||
|
|
721ebaa775 | ||
| 2ccff7dcaa | |||
|
|
4b1b06f00f | ||
|
|
af687fe2d8 | ||
| 9875f22d5a | |||
|
|
260d79fc99 | ||
|
|
1210854c22 | ||
|
|
7ca784d5f5 | ||
|
|
e441f15ee4 | ||
|
|
228004a7e0 | ||
| b20b5c2b8b | |||
|
|
457accfbfa | ||
|
|
43287a4baa | ||
|
|
93fea8ecad | ||
|
|
ba7b0322ff | ||
|
|
473686528e | ||
| c1b47674c8 | |||
|
|
85f234b05e | ||
| 50fa4ed9a5 | |||
|
|
07496e6673 | ||
|
|
9045a8c017 | ||
|
|
3e314a9b9a | ||
| bc4716c48f | |||
|
|
0e69759549 | ||
|
|
88ea7e70da | ||
|
|
7a30f9a29b | ||
| 83fa1ea020 |
114
AGENTS.md
114
AGENTS.md
@@ -33,6 +33,62 @@ packages/api-contract/ # Shared Zod schemas, types, route definitions
|
|||||||
2. Implement handler in `apps/backend`
|
2. Implement handler in `apps/backend`
|
||||||
3. Consume from `apps/frontend` via workspace import `@repo/api-contract`
|
3. Consume from `apps/frontend` via workspace import `@repo/api-contract`
|
||||||
|
|
||||||
|
## Endpoint Pattern
|
||||||
|
|
||||||
|
Every new endpoint must follow the **Result pattern** with the **validate helper**.
|
||||||
|
|
||||||
|
### Files
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `apps/backend/src/lib/result.ts` | `Result<T>` type, `ok()`, `err()` |
|
||||||
|
| `apps/backend/src/lib/errors.ts` | `AppError` discriminated union, `Errors` factory |
|
||||||
|
| `apps/backend/src/lib/http/handle-result.ts` | `handleResult()` — bridges `Result` to HTTP |
|
||||||
|
| `apps/backend/src/lib/http/validate.ts` | `validate.json()`, `.query()`, `.param()`, `.header()`, `.form()` |
|
||||||
|
|
||||||
|
### Layers
|
||||||
|
|
||||||
|
**1. Route** — Use `validate.json(schema)`, `validate.param(schema)`, etc. as Hono middleware.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { validate } from '@/lib/http/validate';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
const idParams = z.object({ id: z.string() });
|
||||||
|
router.post('/', validate.json(createSchema), createHandler);
|
||||||
|
router.get('/:id', validate.param(idParams), getHandler);
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. Service** — Return `Result<T>`. Use `ok(value)` on success, `err(Errors.*(...))` on failure.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Result, ok, err } from '@/lib/result';
|
||||||
|
import { Errors } from '@/lib/errors';
|
||||||
|
|
||||||
|
export async function doSomething(input: Input): Promise<Result<Output>> {
|
||||||
|
if (conflict) return err(Errors.conflict('Already exists'));
|
||||||
|
return ok(result);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**3. Handler** — Call the service and pass the `Result` to `handleResult`.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { handleResult } from '@/lib/http/handle-result';
|
||||||
|
import { doSomething } from './service';
|
||||||
|
|
||||||
|
export async function myHandler(c: AppContext) {
|
||||||
|
const payload = c.req.valid('json') as Input;
|
||||||
|
return handleResult(c, await doSomething(payload), 201);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Never
|
||||||
|
|
||||||
|
- ❌ Throw custom error classes from services
|
||||||
|
- ❌ Use try/catch in handlers for business logic errors
|
||||||
|
- ❌ Use `zValidator` directly — always use `validate.*`
|
||||||
|
|
||||||
## Authentication (Better Auth)
|
## Authentication (Better Auth)
|
||||||
|
|
||||||
The project uses **Better Auth** for session management, replacing the legacy Supabase Auth.
|
The project uses **Better Auth** for session management, replacing the legacy Supabase Auth.
|
||||||
@@ -193,3 +249,61 @@ await getAvailability('my-club', { date: '2026-04-20' });
|
|||||||
1. Create `lib/api/resources/[resource].ts`
|
1. Create `lib/api/resources/[resource].ts`
|
||||||
2. Export functions using `http` from `../http`
|
2. Export functions using `http` from `../http`
|
||||||
3. Add exports in `lib/api/index.ts`
|
3. Add exports in `lib/api/index.ts`
|
||||||
|
|
||||||
|
## Email Templates
|
||||||
|
|
||||||
|
Todos los emails deben usar la misma estética. El layout compartido está en `apps/backend/src/emails/booking-confirmation.ts`.
|
||||||
|
|
||||||
|
### Layout (`wrapLayout`)
|
||||||
|
|
||||||
|
Exportado como `wrapLayout(content)`. Proporciona:
|
||||||
|
- Fondo: `#edf7f4`
|
||||||
|
- Card blanca: `max-width: 520px`, `border-radius: 28px`, `box-shadow: 0 24px 70px rgba(15,23,42,0.12)`, `border: 1px solid rgba(5,9,20,0.1)`
|
||||||
|
- Playzer favicon: `{APP_BASE_URL}/playzer-favicon-512-transparent.png` (está en `apps/frontend/public/`)
|
||||||
|
- **Sin `min-height: 100vh`**: la card empieza arriba, no centrada verticalmente
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { wrapLayout } from '@/emails/booking-confirmation';
|
||||||
|
|
||||||
|
const html = wrapLayout(`<tr>...contenido...</tr>`);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Estructura de cada email
|
||||||
|
|
||||||
|
| Sección | Descripción |
|
||||||
|
|---------|-------------|
|
||||||
|
| **Header** | Dos columnas: badge pill a la izquierda + Playzer (favicon + texto) a la derecha. El badge usa `border-radius: 999px`, `padding: 4px 12px`, `font-size: 11px`, `font-weight: 700`, `letter-spacing: 0.14em`, `text-transform: uppercase`. |
|
||||||
|
| **Card fecha/hora** | Fondo `#f0fdf4`, borde `1px solid rgba(5,150,105,0.3)`, `border-radius: 24px`. Siempre verde aunque el email sea de cancelación. |
|
||||||
|
| **Grilla detalles** | `border: 1px solid #e5e7eb`, `border-radius: 22px`, dos celdas de 50% con `border-right` en la primera. |
|
||||||
|
| **Botón CTA** | Tabla con fondo `#059669`, `border-radius: 12px`, link blanco con `padding: 14px 32px`. |
|
||||||
|
| **Pie** | Sin pie de marca. Solo texto secundario opcional centrado si es necesario. |
|
||||||
|
| **Sin botones de acción** | Los emails de booking **no** incluyen los botones "Compartir por WhatsApp" ni "Hacer otra reserva". |
|
||||||
|
|
||||||
|
### Padding estándar
|
||||||
|
|
||||||
|
| Ubicación | Valor |
|
||||||
|
|-----------|-------|
|
||||||
|
| Wrapper (outer `<td>`) | `padding: 24px 12px` |
|
||||||
|
| Primer `<td>` del contenido (header) | `padding: 24px 20px 16px` |
|
||||||
|
| `<td>` intermedios (cards, texto) | `padding: 0 20px 16px` |
|
||||||
|
| Último `<td>` del contenido | `padding: 0 20px 24px` |
|
||||||
|
|
||||||
|
### Colores de badges según estado
|
||||||
|
|
||||||
|
| Estado | Fondo badge | Texto badge |
|
||||||
|
|--------|-------------|-------------|
|
||||||
|
| Confirmado | `#f0fdf4` | `#15803d` |
|
||||||
|
| Cancelado | `#fef2f2` | `#dc2626` |
|
||||||
|
| No concretado | `#fffbeb` | `#d97706` |
|
||||||
|
| Neutro (verificación, etc.) | `#f4f4f5` | `#71717a` |
|
||||||
|
|
||||||
|
### Archivos de templates
|
||||||
|
|
||||||
|
| Archivo | Templates |
|
||||||
|
|---------|-----------|
|
||||||
|
| `apps/backend/src/emails/booking-confirmation.ts` | `bookingConfirmationHtml`, `bookingCancelledHtml`, `bookingNoShowHtml` + exporta `wrapLayout` |
|
||||||
|
| `apps/backend/src/lib/auth.ts` | Email de verificación de Better Auth (usa `wrapLayout` inline) |
|
||||||
|
|
||||||
|
### Regla general
|
||||||
|
|
||||||
|
Para emails nuevos: importar `wrapLayout`, construir el HTML interno con `<tr>`s, usar la misma estructura de cabecera (badge + Playzer), y nunca incluir el pie "Playzer — Reserva de canchas online". Usar `APP_BASE_URL` para construir URLs absolutas al logo.
|
||||||
|
|||||||
5
apps/backend/.gitignore
vendored
5
apps/backend/.gitignore
vendored
@@ -1,3 +1,6 @@
|
|||||||
# deps
|
# deps
|
||||||
node_modules/
|
node_modules/
|
||||||
prisma.config.prod.ts
|
prisma.config.prod.ts
|
||||||
|
|
||||||
|
# build output
|
||||||
|
dist/
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
"dev": "bun run --hot src/server.ts",
|
"dev": "bun run --hot src/server.ts",
|
||||||
"start": "bun src/server.ts",
|
"start": "bun src/server.ts",
|
||||||
"build": "tsc -b",
|
"build": "tsc -b",
|
||||||
"test": "bun test",
|
"test": "bun test --preload ./test/support/prisma.mock.ts ./test",
|
||||||
"lint": "biome check .",
|
"lint": "biome check .",
|
||||||
"lint:fix": "biome check --write .",
|
"lint:fix": "biome check --write .",
|
||||||
"format": "biome format --write .",
|
"format": "biome format --write .",
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ model User {
|
|||||||
emailVerified Boolean @default(false)
|
emailVerified Boolean @default(false)
|
||||||
image String?
|
image String?
|
||||||
phone String?
|
phone String?
|
||||||
|
banned Boolean @default(false)
|
||||||
|
bannedAt DateTime?
|
||||||
|
banReason String?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
sessions Session[]
|
sessions Session[]
|
||||||
@@ -17,15 +20,20 @@ model User {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model Session {
|
model Session {
|
||||||
id String @id
|
id String @id
|
||||||
expiresAt DateTime
|
expiresAt DateTime
|
||||||
token String
|
token String
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
ipAddress String?
|
ipAddress String?
|
||||||
userAgent String?
|
userAgent String?
|
||||||
userId String
|
country String?
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
city String?
|
||||||
|
countryCode String?
|
||||||
|
latitude Float?
|
||||||
|
longitude Float?
|
||||||
|
userId String
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
@@unique([token])
|
@@unique([token])
|
||||||
@@index([userId])
|
@@index([userId])
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ model Complex {
|
|||||||
users ComplexUser[]
|
users ComplexUser[]
|
||||||
invitations ComplexInvitation[]
|
invitations ComplexInvitation[]
|
||||||
courts Court[]
|
courts Court[]
|
||||||
|
recurringGroups RecurringBookingGroup[]
|
||||||
|
|
||||||
@@index([planCode])
|
@@index([planCode])
|
||||||
@@index([complexSlug])
|
@@index([complexSlug])
|
||||||
|
|||||||
@@ -15,6 +15,11 @@ enum CourtBookingStatus {
|
|||||||
NOSHOW
|
NOSHOW
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum RecurringBookingGroupStatus {
|
||||||
|
ACTIVE
|
||||||
|
CANCELLED
|
||||||
|
}
|
||||||
|
|
||||||
model Sport {
|
model Sport {
|
||||||
id String @id @db.Uuid
|
id String @id @db.Uuid
|
||||||
name String @unique
|
name String @unique
|
||||||
@@ -34,6 +39,8 @@ model Court {
|
|||||||
name String
|
name String
|
||||||
slotDurationMinutes Int @map("slot_duration_minutes")
|
slotDurationMinutes Int @map("slot_duration_minutes")
|
||||||
basePrice Decimal @map("base_price") @db.Decimal(19, 2)
|
basePrice Decimal @map("base_price") @db.Decimal(19, 2)
|
||||||
|
isUnderMaintenance Boolean @default(false) @map("is_under_maintenance")
|
||||||
|
maintenanceReason String? @map("maintenance_reason") @db.VarChar(500)
|
||||||
createdAt DateTime @default(now()) @map("created_at")
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
updatedAt DateTime @updatedAt @map("updated_at")
|
updatedAt DateTime @updatedAt @map("updated_at")
|
||||||
complex Complex @relation(fields: [complexId], references: [id], onDelete: Cascade)
|
complex Complex @relation(fields: [complexId], references: [id], onDelete: Cascade)
|
||||||
@@ -41,12 +48,29 @@ model Court {
|
|||||||
availabilities CourtAvailability[]
|
availabilities CourtAvailability[]
|
||||||
priceRules CourtPriceRule[]
|
priceRules CourtPriceRule[]
|
||||||
bookings CourtBooking[]
|
bookings CourtBooking[]
|
||||||
|
maintenances CourtMaintenance[]
|
||||||
|
recurringGroups RecurringBookingGroup[]
|
||||||
|
|
||||||
@@index([complexId])
|
@@index([complexId])
|
||||||
@@index([sportId])
|
@@index([sportId])
|
||||||
@@map("courts")
|
@@map("courts")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model CourtMaintenance {
|
||||||
|
id String @id @db.Uuid
|
||||||
|
courtId String @map("court_id") @db.Uuid
|
||||||
|
startDate DateTime @map("start_date") @db.Date
|
||||||
|
startTime String? @map("start_time") @db.VarChar(5)
|
||||||
|
endTime String? @map("end_time") @db.VarChar(5)
|
||||||
|
reason String? @db.VarChar(500)
|
||||||
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
|
updatedAt DateTime @updatedAt @map("updated_at")
|
||||||
|
court Court @relation(fields: [courtId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@index([courtId, startDate])
|
||||||
|
@@map("court_maintenances")
|
||||||
|
}
|
||||||
|
|
||||||
model CourtAvailability {
|
model CourtAvailability {
|
||||||
id String @id @db.Uuid
|
id String @id @db.Uuid
|
||||||
courtId String @map("court_id") @db.Uuid
|
courtId String @map("court_id") @db.Uuid
|
||||||
@@ -77,22 +101,26 @@ model CourtPriceRule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model CourtBooking {
|
model CourtBooking {
|
||||||
id String @id @db.Uuid
|
id String @id @db.Uuid
|
||||||
bookingCode String @unique @map("booking_code") @db.VarChar(8)
|
bookingCode String @unique @map("booking_code") @db.VarChar(8)
|
||||||
courtId String @map("court_id") @db.Uuid
|
courtId String @map("court_id") @db.Uuid
|
||||||
bookingDate DateTime @map("booking_date") @db.Date
|
bookingDate DateTime @map("booking_date") @db.Date
|
||||||
startTime String @map("start_time") @db.VarChar(5)
|
startTime String @map("start_time") @db.VarChar(5)
|
||||||
endTime String @map("end_time") @db.VarChar(5)
|
endTime String @map("end_time") @db.VarChar(5)
|
||||||
customerName String @map("customer_name") @db.VarChar(120)
|
customerName String @map("customer_name") @db.VarChar(120)
|
||||||
customerPhone String @map("customer_phone") @db.VarChar(30)
|
customerPhone String @map("customer_phone") @db.VarChar(30)
|
||||||
status CourtBookingStatus @default(CONFIRMED)
|
customerEmail String @map("customer_email") @db.VarChar(254)
|
||||||
createdAt DateTime @default(now()) @map("created_at")
|
status CourtBookingStatus @default(CONFIRMED)
|
||||||
updatedAt DateTime @updatedAt @map("updated_at")
|
recurringGroupId String? @map("recurring_group_id") @db.Uuid
|
||||||
court Court @relation(fields: [courtId], references: [id], onDelete: Cascade)
|
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])
|
@@unique([courtId, bookingDate, startTime])
|
||||||
@@index([courtId, bookingDate])
|
@@index([courtId, bookingDate])
|
||||||
@@index([bookingDate])
|
@@index([bookingDate])
|
||||||
|
@@index([recurringGroupId])
|
||||||
@@map("court_bookings")
|
@@map("court_bookings")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,8 +135,34 @@ model CourtBookingLog {
|
|||||||
newStatus CourtBookingStatus @map("new_status")
|
newStatus CourtBookingStatus @map("new_status")
|
||||||
customerName String @map("customer_name") @db.VarChar(120)
|
customerName String @map("customer_name") @db.VarChar(120)
|
||||||
customerPhone String @map("customer_phone") @db.VarChar(30)
|
customerPhone String @map("customer_phone") @db.VarChar(30)
|
||||||
|
customerEmail String @map("customer_email") @db.VarChar(254)
|
||||||
changedAt DateTime @default(now()) @map("changed_at")
|
changedAt DateTime @default(now()) @map("changed_at")
|
||||||
|
|
||||||
@@map("court_booking_logs")
|
@@map("court_booking_logs")
|
||||||
@@index([courtId, newStatus])
|
@@index([courtId, newStatus])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model RecurringBookingGroup {
|
||||||
|
id String @id @db.Uuid
|
||||||
|
complexId String @map("complex_id") @db.Uuid
|
||||||
|
courtId String @map("court_id") @db.Uuid
|
||||||
|
startTime String @map("start_time") @db.VarChar(5)
|
||||||
|
endTime String @map("end_time") @db.VarChar(5)
|
||||||
|
dayOfWeek DayOfWeek @map("day_of_week")
|
||||||
|
startDate DateTime @map("start_date") @db.Date
|
||||||
|
endDate DateTime? @map("end_date") @db.Date
|
||||||
|
status RecurringBookingGroupStatus @default(ACTIVE)
|
||||||
|
customerName String @map("customer_name") @db.VarChar(120)
|
||||||
|
customerPhone String @map("customer_phone") @db.VarChar(30)
|
||||||
|
customerEmail String @map("customer_email") @db.VarChar(254)
|
||||||
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
|
updatedAt DateTime @updatedAt @map("updated_at")
|
||||||
|
court Court @relation(fields: [courtId], references: [id], onDelete: Cascade)
|
||||||
|
complex Complex @relation(fields: [complexId], references: [id], onDelete: Cascade)
|
||||||
|
bookings CourtBooking[]
|
||||||
|
|
||||||
|
@@index([complexId])
|
||||||
|
@@index([courtId])
|
||||||
|
@@index([status])
|
||||||
|
@@map("recurring_booking_groups")
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the `onboarding_requests` table. If the table is not empty, all the data it contains will be lost.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "court_booking_logs" ADD COLUMN "customer_email" VARCHAR(254);
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "court_bookings" ADD COLUMN "customer_email" VARCHAR(254);
|
||||||
|
|
||||||
|
-- DropTable
|
||||||
|
DROP TABLE "onboarding_requests";
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- Made the column `customer_email` on table `court_booking_logs` required. This step will fail if there are existing NULL values in that column.
|
||||||
|
- Made the column `customer_email` on table `court_bookings` required. This step will fail if there are existing NULL values in that column.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- Backfill existing NULL values with placeholder
|
||||||
|
UPDATE "court_booking_logs" SET "customer_email" = 'missing@playzer.app' WHERE "customer_email" IS NULL;
|
||||||
|
UPDATE "court_bookings" SET "customer_email" = 'missing@playzer.app' WHERE "customer_email" IS NULL;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "court_booking_logs" ALTER COLUMN "customer_email" SET NOT NULL;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "court_bookings" ALTER COLUMN "customer_email" SET NOT NULL;
|
||||||
@@ -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;
|
||||||
@@ -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);
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -4,7 +4,7 @@ export const planSeeds = [
|
|||||||
name: 'Basic',
|
name: 'Basic',
|
||||||
price: '49.00',
|
price: '49.00',
|
||||||
rules: {
|
rules: {
|
||||||
version: 'v1',
|
version: 'v2',
|
||||||
limits: {
|
limits: {
|
||||||
maxCourts: 2,
|
maxCourts: 2,
|
||||||
maxBookingsPerDay: 80,
|
maxBookingsPerDay: 80,
|
||||||
@@ -21,6 +21,12 @@ export const planSeeds = [
|
|||||||
publicBookingPage: true,
|
publicBookingPage: true,
|
||||||
advancedReports: false,
|
advancedReports: false,
|
||||||
whatsappReminders: false,
|
whatsappReminders: false,
|
||||||
|
fixedSlots: false,
|
||||||
|
},
|
||||||
|
pricing: {
|
||||||
|
overrides: {
|
||||||
|
AR: { amount: 14999, currency: 'ARS' },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -29,7 +35,7 @@ export const planSeeds = [
|
|||||||
name: 'Advanced',
|
name: 'Advanced',
|
||||||
price: '99.00',
|
price: '99.00',
|
||||||
rules: {
|
rules: {
|
||||||
version: 'v1',
|
version: 'v2',
|
||||||
limits: {
|
limits: {
|
||||||
maxCourts: 5,
|
maxCourts: 5,
|
||||||
maxBookingsPerDay: 220,
|
maxBookingsPerDay: 220,
|
||||||
@@ -46,6 +52,12 @@ export const planSeeds = [
|
|||||||
publicBookingPage: true,
|
publicBookingPage: true,
|
||||||
advancedReports: true,
|
advancedReports: true,
|
||||||
whatsappReminders: true,
|
whatsappReminders: true,
|
||||||
|
fixedSlots: true,
|
||||||
|
},
|
||||||
|
pricing: {
|
||||||
|
overrides: {
|
||||||
|
AR: { amount: 29999, currency: 'ARS' },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -54,7 +66,7 @@ export const planSeeds = [
|
|||||||
name: 'Enterprise',
|
name: 'Enterprise',
|
||||||
price: '199.00',
|
price: '199.00',
|
||||||
rules: {
|
rules: {
|
||||||
version: 'v1',
|
version: 'v2',
|
||||||
limits: {
|
limits: {
|
||||||
maxCourts: 20,
|
maxCourts: 20,
|
||||||
maxBookingsPerDay: 2000,
|
maxBookingsPerDay: 2000,
|
||||||
@@ -71,6 +83,12 @@ export const planSeeds = [
|
|||||||
publicBookingPage: true,
|
publicBookingPage: true,
|
||||||
advancedReports: true,
|
advancedReports: true,
|
||||||
whatsappReminders: true,
|
whatsappReminders: true,
|
||||||
|
fixedSlots: true,
|
||||||
|
},
|
||||||
|
pricing: {
|
||||||
|
overrides: {
|
||||||
|
AR: { amount: 59999, currency: 'ARS' },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
401
apps/backend/src/emails/booking-confirmation.ts
Normal file
401
apps/backend/src/emails/booking-confirmation.ts
Normal file
@@ -0,0 +1,401 @@
|
|||||||
|
type BookingEmailData = {
|
||||||
|
bookingCode: string;
|
||||||
|
complexSlug?: string;
|
||||||
|
complexName: string;
|
||||||
|
date: string;
|
||||||
|
startTime: string;
|
||||||
|
endTime: string;
|
||||||
|
courtName: string;
|
||||||
|
sportName: string;
|
||||||
|
customerName: string;
|
||||||
|
price?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const APP_BASE_URL = process.env.APP_BASE_URL ?? 'http://localhost:5173';
|
||||||
|
|
||||||
|
function formatBookingPrice(price: number): string {
|
||||||
|
if (price === 0) {
|
||||||
|
return 'Sin cargo';
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Intl.NumberFormat('es-AR', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'ARS',
|
||||||
|
maximumFractionDigits: Number.isInteger(price) ? 0 : 2,
|
||||||
|
}).format(price);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatFriendlyDate(isoDate: string): string {
|
||||||
|
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(isoDate);
|
||||||
|
if (!match) return isoDate;
|
||||||
|
|
||||||
|
const date = new Date(Number(match[1]), Number(match[2]) - 1, Number(match[3]));
|
||||||
|
|
||||||
|
const weekday = new Intl.DateTimeFormat('es-AR', { weekday: 'long' }).format(date);
|
||||||
|
const day = date.getDate();
|
||||||
|
const month = new Intl.DateTimeFormat('es-AR', { month: 'long' }).format(date);
|
||||||
|
|
||||||
|
return `${weekday.charAt(0).toUpperCase() + weekday.slice(1)}, ${day} de ${month}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BASE_STYLES = `
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background-color: #edf7f4;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||||
|
}
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export function wrapLayout(content: string) {
|
||||||
|
return `<!DOCTYPE html>
|
||||||
|
<html lang="es">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Playzer</title>
|
||||||
|
<style>${BASE_STYLES}</style>
|
||||||
|
</head>
|
||||||
|
<body style="margin:0;padding:0;background-color:#edf7f4;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif;">
|
||||||
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color:#edf7f4;">
|
||||||
|
<tr>
|
||||||
|
<td align="center" style="padding:24px 12px;">
|
||||||
|
<table role="presentation" width="100%" style="max-width:520px;background-color:#ffffff;border-radius:28px;overflow:hidden;box-shadow:0 24px 70px rgba(15,23,42,0.12);border:1px solid rgba(5,9,20,0.1);">
|
||||||
|
${content}
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function bookingConfirmationHtml(data: BookingEmailData): string {
|
||||||
|
const formattedPrice = formatBookingPrice(data.price ?? 0);
|
||||||
|
const friendlyDate = formatFriendlyDate(data.date);
|
||||||
|
|
||||||
|
const content = `
|
||||||
|
<tr>
|
||||||
|
<td style="padding:24px 20px 16px;">
|
||||||
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0">
|
||||||
|
<tr>
|
||||||
|
<td valign="top">
|
||||||
|
<table role="presentation" cellpadding="0" cellspacing="0" style="display:inline-block;background-color:#f0fdf4;border-radius:999px;padding:4px 12px;">
|
||||||
|
<tr>
|
||||||
|
<td style="font-size:11px;font-weight:700;letter-spacing:0.14em;color:#15803d;text-transform:uppercase;line-height:1.25rem;">
|
||||||
|
✓ Reserva confirmada
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<h1 style="margin:12px 0 0;font-size:28px;font-weight:700;color:#111827;letter-spacing:-0.025em;">
|
||||||
|
${data.complexName}
|
||||||
|
</h1>
|
||||||
|
</td>
|
||||||
|
<td valign="top" align="right" style="white-space:nowrap;">
|
||||||
|
<table role="presentation" cellpadding="0" cellspacing="0">
|
||||||
|
<tr>
|
||||||
|
<td valign="middle" style="padding-right:8px;">
|
||||||
|
<img src="${APP_BASE_URL}/playzer-favicon-512-transparent.png" alt="Playzer" width="32" height="32" style="display:block;" />
|
||||||
|
</td>
|
||||||
|
<td valign="middle">
|
||||||
|
<span style="font-size:18px;font-weight:700;color:#475569;letter-spacing:-0.025em;">Playzer</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td style="padding:0 20px 16px;">
|
||||||
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color:#f0fdf4;border-radius:24px;border:1px solid rgba(5,150,105,0.3);">
|
||||||
|
<tr>
|
||||||
|
<td style="padding:20px 24px;">
|
||||||
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0">
|
||||||
|
<tr>
|
||||||
|
<td valign="bottom">
|
||||||
|
<p style="margin:0;font-size:13px;font-weight:600;color:#15803d;">
|
||||||
|
Tu turno
|
||||||
|
</p>
|
||||||
|
<p style="margin:12px 0 0;font-size:28px;font-weight:900;color:#111827;line-height:1.1;letter-spacing:-0.025em;">
|
||||||
|
${friendlyDate}
|
||||||
|
</p>
|
||||||
|
<p style="margin:8px 0 0;font-size:24px;font-weight:900;color:#059669;line-height:1;letter-spacing:-0.025em;">
|
||||||
|
${data.startTime} — ${data.endTime}
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
<td valign="bottom" align="right" style="padding-left:16px;">
|
||||||
|
<table role="presentation" cellpadding="0" cellspacing="0" style="background-color:rgba(255,255,255,0.8);border-radius:16px;border:1px solid rgba(5,150,105,0.2);">
|
||||||
|
<tr>
|
||||||
|
<td style="padding:12px 16px;text-align:right;min-width:120px;">
|
||||||
|
<p style="margin:0;font-size:11px;font-weight:500;color:#6b7280;">Precio del turno</p>
|
||||||
|
<p style="margin:4px 0 0;font-size:20px;font-weight:700;color:#111827;">${formattedPrice}</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td style="padding:0 20px 16px;">
|
||||||
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="border:1px solid #e5e7eb;border-radius:22px;overflow:hidden;">
|
||||||
|
<tr>
|
||||||
|
<td width="50%" style="padding:16px;border-right:1px solid #e5e7eb;background-color:#ffffff;">
|
||||||
|
<p style="margin:0;font-size:13px;font-weight:500;color:#6b7280;">
|
||||||
|
Cancha
|
||||||
|
</p>
|
||||||
|
<p style="margin:8px 0 0;font-size:14px;font-weight:600;color:#111827;">
|
||||||
|
${data.courtName} — ${data.sportName}
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
<td width="50%" style="padding:16px;background-color:#ffffff;">
|
||||||
|
<p style="margin:0;font-size:13px;font-weight:500;color:#6b7280;">
|
||||||
|
Código de reserva
|
||||||
|
</p>
|
||||||
|
<p style="margin:8px 0 0;font-family:monospace;font-size:16px;font-weight:700;letter-spacing:0.18em;color:#111827;">
|
||||||
|
${data.bookingCode}
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
${
|
||||||
|
data.complexSlug
|
||||||
|
? `
|
||||||
|
<tr>
|
||||||
|
<td style="padding:0 20px 24px;">
|
||||||
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0">
|
||||||
|
<tr>
|
||||||
|
<td align="center">
|
||||||
|
<table role="presentation" cellpadding="0" cellspacing="0" style="background-color:#059669;border-radius:12px;">
|
||||||
|
<tr>
|
||||||
|
<td style="padding:14px 32px;font-size:15px;font-weight:600;">
|
||||||
|
<a href="${APP_BASE_URL}/${data.complexSlug}/booking/confirmed/${data.bookingCode}" style="color:#ffffff;text-decoration:none;display:inline-block;">
|
||||||
|
Cancelar reserva
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>`
|
||||||
|
: ''
|
||||||
|
}`;
|
||||||
|
|
||||||
|
return wrapLayout(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function bookingCancelledHtml(data: BookingEmailData): string {
|
||||||
|
const friendlyDate = formatFriendlyDate(data.date);
|
||||||
|
|
||||||
|
const content = `
|
||||||
|
<tr>
|
||||||
|
<td style="padding:24px 20px 16px;">
|
||||||
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0">
|
||||||
|
<tr>
|
||||||
|
<td valign="top">
|
||||||
|
<table role="presentation" cellpadding="0" cellspacing="0" style="display:inline-block;background-color:#fef2f2;border-radius:999px;padding:4px 12px;">
|
||||||
|
<tr>
|
||||||
|
<td style="font-size:11px;font-weight:700;letter-spacing:0.14em;color:#dc2626;text-transform:uppercase;line-height:1.25rem;">
|
||||||
|
✗ Reserva cancelada
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<h1 style="margin:12px 0 0;font-size:28px;font-weight:700;color:#111827;letter-spacing:-0.025em;">
|
||||||
|
${data.complexName}
|
||||||
|
</h1>
|
||||||
|
</td>
|
||||||
|
<td valign="top" align="right" style="white-space:nowrap;">
|
||||||
|
<table role="presentation" cellpadding="0" cellspacing="0">
|
||||||
|
<tr>
|
||||||
|
<td valign="middle" style="padding-right:8px;">
|
||||||
|
<img src="${APP_BASE_URL}/playzer-favicon-512-transparent.png" alt="Playzer" width="32" height="32" style="display:block;" />
|
||||||
|
</td>
|
||||||
|
<td valign="middle">
|
||||||
|
<span style="font-size:18px;font-weight:700;color:#475569;letter-spacing:-0.025em;">Playzer</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td style="padding:0 20px 16px;">
|
||||||
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color:#f0fdf4;border-radius:24px;border:1px solid rgba(5,150,105,0.3);">
|
||||||
|
<tr>
|
||||||
|
<td style="padding:20px 24px;">
|
||||||
|
<p style="margin:0;font-size:13px;font-weight:600;color:#15803d;">
|
||||||
|
Tu turno
|
||||||
|
</p>
|
||||||
|
<p style="margin:12px 0 0;font-size:28px;font-weight:900;color:#111827;line-height:1.1;letter-spacing:-0.025em;">
|
||||||
|
${friendlyDate}
|
||||||
|
</p>
|
||||||
|
<p style="margin:8px 0 0;font-size:24px;font-weight:900;color:#059669;line-height:1;letter-spacing:-0.025em;">
|
||||||
|
${data.startTime} — ${data.endTime}
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td style="padding:0 20px 16px;">
|
||||||
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color:#fef2f2;border-radius:24px;border:1px solid rgba(220,38,38,0.3);">
|
||||||
|
<tr>
|
||||||
|
<td style="padding:20px 24px;">
|
||||||
|
<p style="margin:0;font-size:14px;color:#374151;line-height:1.6;">
|
||||||
|
La reserva <strong style="font-family:monospace;font-weight:700;letter-spacing:2px;">${data.bookingCode}</strong>
|
||||||
|
fue cancelada. Si tenés dudas, contactate con el complejo.
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td style="padding:0 20px 24px;">
|
||||||
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="border:1px solid #e5e7eb;border-radius:22px;overflow:hidden;">
|
||||||
|
<tr>
|
||||||
|
<td width="50%" style="padding:16px;border-right:1px solid #e5e7eb;background-color:#ffffff;">
|
||||||
|
<p style="margin:0;font-size:13px;font-weight:500;color:#6b7280;">
|
||||||
|
Cancha
|
||||||
|
</p>
|
||||||
|
<p style="margin:8px 0 0;font-size:14px;font-weight:600;color:#111827;">
|
||||||
|
${data.courtName} — ${data.sportName}
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
<td width="50%" style="padding:16px;background-color:#ffffff;">
|
||||||
|
<p style="margin:0;font-size:13px;font-weight:500;color:#6b7280;">
|
||||||
|
Cliente
|
||||||
|
</p>
|
||||||
|
<p style="margin:8px 0 0;font-size:14px;font-weight:600;color:#111827;">
|
||||||
|
${data.customerName}
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>`;
|
||||||
|
|
||||||
|
return wrapLayout(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function bookingNoShowHtml(data: BookingEmailData): string {
|
||||||
|
const friendlyDate = formatFriendlyDate(data.date);
|
||||||
|
|
||||||
|
const content = `
|
||||||
|
<tr>
|
||||||
|
<td style="padding:24px 20px 16px;">
|
||||||
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0">
|
||||||
|
<tr>
|
||||||
|
<td valign="top">
|
||||||
|
<table role="presentation" cellpadding="0" cellspacing="0" style="display:inline-block;background-color:#fffbeb;border-radius:999px;padding:4px 12px;">
|
||||||
|
<tr>
|
||||||
|
<td style="font-size:11px;font-weight:700;letter-spacing:0.14em;color:#d97706;text-transform:uppercase;line-height:1.25rem;">
|
||||||
|
Reserva no concretada
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<h1 style="margin:12px 0 0;font-size:28px;font-weight:700;color:#111827;letter-spacing:-0.025em;">
|
||||||
|
${data.complexName}
|
||||||
|
</h1>
|
||||||
|
</td>
|
||||||
|
<td valign="top" align="right" style="white-space:nowrap;">
|
||||||
|
<table role="presentation" cellpadding="0" cellspacing="0">
|
||||||
|
<tr>
|
||||||
|
<td valign="middle" style="padding-right:8px;">
|
||||||
|
<img src="${APP_BASE_URL}/playzer-favicon-512-transparent.png" alt="Playzer" width="32" height="32" style="display:block;" />
|
||||||
|
</td>
|
||||||
|
<td valign="middle">
|
||||||
|
<span style="font-size:18px;font-weight:700;color:#475569;letter-spacing:-0.025em;">Playzer</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td style="padding:0 20px 16px;">
|
||||||
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color:#f0fdf4;border-radius:24px;border:1px solid rgba(5,150,105,0.3);">
|
||||||
|
<tr>
|
||||||
|
<td style="padding:20px 24px;">
|
||||||
|
<p style="margin:0;font-size:13px;font-weight:600;color:#15803d;">
|
||||||
|
Tu turno
|
||||||
|
</p>
|
||||||
|
<p style="margin:12px 0 0;font-size:28px;font-weight:900;color:#111827;line-height:1.1;letter-spacing:-0.025em;">
|
||||||
|
${friendlyDate}
|
||||||
|
</p>
|
||||||
|
<p style="margin:8px 0 0;font-size:24px;font-weight:900;color:#059669;line-height:1;letter-spacing:-0.025em;">
|
||||||
|
${data.startTime} — ${data.endTime}
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td style="padding:0 20px 16px;">
|
||||||
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color:#fffbeb;border-radius:24px;border:1px solid rgba(217,119,6,0.3);">
|
||||||
|
<tr>
|
||||||
|
<td style="padding:20px 24px;">
|
||||||
|
<p style="margin:0;font-size:14px;color:#374151;line-height:1.6;">
|
||||||
|
La reserva <strong style="font-family:monospace;font-weight:700;letter-spacing:2px;">${data.bookingCode}</strong>
|
||||||
|
fue registrada como no concretada por falta de asistencia.
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td style="padding:0 20px 24px;">
|
||||||
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="border:1px solid #e5e7eb;border-radius:22px;overflow:hidden;">
|
||||||
|
<tr>
|
||||||
|
<td width="50%" style="padding:16px;border-right:1px solid #e5e7eb;background-color:#ffffff;">
|
||||||
|
<p style="margin:0;font-size:13px;font-weight:500;color:#6b7280;">
|
||||||
|
Cancha
|
||||||
|
</p>
|
||||||
|
<p style="margin:8px 0 0;font-size:14px;font-weight:600;color:#111827;">
|
||||||
|
${data.courtName} — ${data.sportName}
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
<td width="50%" style="padding:16px;background-color:#ffffff;">
|
||||||
|
<p style="margin:0;font-size:13px;font-weight:500;color:#6b7280;">
|
||||||
|
Cliente
|
||||||
|
</p>
|
||||||
|
<p style="margin:8px 0 0;font-size:14px;font-weight:600;color:#111827;">
|
||||||
|
${data.customerName}
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>`;
|
||||||
|
|
||||||
|
return wrapLayout(content);
|
||||||
|
}
|
||||||
@@ -62,6 +62,11 @@ export type Sport = Prisma.SportModel
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export type Court = Prisma.CourtModel
|
export type Court = Prisma.CourtModel
|
||||||
|
/**
|
||||||
|
* Model CourtMaintenance
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type CourtMaintenance = Prisma.CourtMaintenanceModel
|
||||||
/**
|
/**
|
||||||
* Model CourtAvailability
|
* Model CourtAvailability
|
||||||
*
|
*
|
||||||
@@ -83,10 +88,10 @@ export type CourtBooking = Prisma.CourtBookingModel
|
|||||||
*/
|
*/
|
||||||
export type CourtBookingLog = Prisma.CourtBookingLogModel
|
export type CourtBookingLog = Prisma.CourtBookingLogModel
|
||||||
/**
|
/**
|
||||||
* Model OnboardingRequest
|
* Model RecurringBookingGroup
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export type OnboardingRequest = Prisma.OnboardingRequestModel
|
export type RecurringBookingGroup = Prisma.RecurringBookingGroupModel
|
||||||
/**
|
/**
|
||||||
* Model PasswordResetRequest
|
* Model PasswordResetRequest
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -86,6 +86,11 @@ export type Sport = Prisma.SportModel
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export type Court = Prisma.CourtModel
|
export type Court = Prisma.CourtModel
|
||||||
|
/**
|
||||||
|
* Model CourtMaintenance
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type CourtMaintenance = Prisma.CourtMaintenanceModel
|
||||||
/**
|
/**
|
||||||
* Model CourtAvailability
|
* Model CourtAvailability
|
||||||
*
|
*
|
||||||
@@ -107,10 +112,10 @@ export type CourtBooking = Prisma.CourtBookingModel
|
|||||||
*/
|
*/
|
||||||
export type CourtBookingLog = Prisma.CourtBookingLogModel
|
export type CourtBookingLog = Prisma.CourtBookingLogModel
|
||||||
/**
|
/**
|
||||||
* Model OnboardingRequest
|
* Model RecurringBookingGroup
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export type OnboardingRequest = Prisma.OnboardingRequestModel
|
export type RecurringBookingGroup = Prisma.RecurringBookingGroupModel
|
||||||
/**
|
/**
|
||||||
* Model PasswordResetRequest
|
* Model PasswordResetRequest
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -49,6 +49,17 @@ export type StringNullableFilter<$PrismaModel = never> = {
|
|||||||
not?: Prisma.NestedStringNullableFilter<$PrismaModel> | string | null
|
not?: Prisma.NestedStringNullableFilter<$PrismaModel> | string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type DateTimeNullableFilter<$PrismaModel = never> = {
|
||||||
|
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
|
||||||
|
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
|
||||||
|
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
|
||||||
|
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedDateTimeNullableFilter<$PrismaModel> | Date | string | null
|
||||||
|
}
|
||||||
|
|
||||||
export type DateTimeFilter<$PrismaModel = never> = {
|
export type DateTimeFilter<$PrismaModel = never> = {
|
||||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
|
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
|
||||||
@@ -109,6 +120,20 @@ export type StringNullableWithAggregatesFilter<$PrismaModel = never> = {
|
|||||||
_max?: Prisma.NestedStringNullableFilter<$PrismaModel>
|
_max?: Prisma.NestedStringNullableFilter<$PrismaModel>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type DateTimeNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
|
||||||
|
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
|
||||||
|
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
|
||||||
|
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedDateTimeNullableWithAggregatesFilter<$PrismaModel> | Date | string | null
|
||||||
|
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
export type DateTimeWithAggregatesFilter<$PrismaModel = never> = {
|
export type DateTimeWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
|
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
|
||||||
@@ -123,29 +148,31 @@ export type DateTimeWithAggregatesFilter<$PrismaModel = never> = {
|
|||||||
_max?: Prisma.NestedDateTimeFilter<$PrismaModel>
|
_max?: Prisma.NestedDateTimeFilter<$PrismaModel>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DateTimeNullableFilter<$PrismaModel = never> = {
|
export type FloatNullableFilter<$PrismaModel = never> = {
|
||||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
|
equals?: number | Prisma.FloatFieldRefInput<$PrismaModel> | null
|
||||||
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
|
in?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel> | null
|
||||||
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
|
notIn?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel> | null
|
||||||
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
lt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||||
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
lte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||||
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
gt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||||
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
gte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||||
not?: Prisma.NestedDateTimeNullableFilter<$PrismaModel> | Date | string | null
|
not?: Prisma.NestedFloatNullableFilter<$PrismaModel> | number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DateTimeNullableWithAggregatesFilter<$PrismaModel = never> = {
|
export type FloatNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
|
equals?: number | Prisma.FloatFieldRefInput<$PrismaModel> | null
|
||||||
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
|
in?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel> | null
|
||||||
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
|
notIn?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel> | null
|
||||||
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
lt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||||
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
lte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||||
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
gt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||||
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
gte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||||
not?: Prisma.NestedDateTimeNullableWithAggregatesFilter<$PrismaModel> | Date | string | null
|
not?: Prisma.NestedFloatNullableWithAggregatesFilter<$PrismaModel> | number | null
|
||||||
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||||
_min?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
|
_avg?: Prisma.NestedFloatNullableFilter<$PrismaModel>
|
||||||
_max?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
|
_sum?: Prisma.NestedFloatNullableFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedFloatNullableFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedFloatNullableFilter<$PrismaModel>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UuidFilter<$PrismaModel = never> = {
|
export type UuidFilter<$PrismaModel = never> = {
|
||||||
@@ -287,6 +314,18 @@ export type EnumCourtBookingStatusFilter<$PrismaModel = never> = {
|
|||||||
not?: Prisma.NestedEnumCourtBookingStatusFilter<$PrismaModel> | $Enums.CourtBookingStatus
|
not?: Prisma.NestedEnumCourtBookingStatusFilter<$PrismaModel> | $Enums.CourtBookingStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type UuidNullableFilter<$PrismaModel = never> = {
|
||||||
|
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
|
||||||
|
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
|
||||||
|
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
|
||||||
|
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
mode?: Prisma.QueryMode
|
||||||
|
not?: Prisma.NestedUuidNullableFilter<$PrismaModel> | string | null
|
||||||
|
}
|
||||||
|
|
||||||
export type EnumCourtBookingStatusWithAggregatesFilter<$PrismaModel = never> = {
|
export type EnumCourtBookingStatusWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
equals?: $Enums.CourtBookingStatus | Prisma.EnumCourtBookingStatusFieldRefInput<$PrismaModel>
|
equals?: $Enums.CourtBookingStatus | Prisma.EnumCourtBookingStatusFieldRefInput<$PrismaModel>
|
||||||
in?: $Enums.CourtBookingStatus[] | Prisma.ListEnumCourtBookingStatusFieldRefInput<$PrismaModel>
|
in?: $Enums.CourtBookingStatus[] | Prisma.ListEnumCourtBookingStatusFieldRefInput<$PrismaModel>
|
||||||
@@ -297,6 +336,38 @@ export type EnumCourtBookingStatusWithAggregatesFilter<$PrismaModel = never> = {
|
|||||||
_max?: Prisma.NestedEnumCourtBookingStatusFilter<$PrismaModel>
|
_max?: Prisma.NestedEnumCourtBookingStatusFilter<$PrismaModel>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type UuidNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
|
||||||
|
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
|
||||||
|
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
|
||||||
|
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
mode?: Prisma.QueryMode
|
||||||
|
not?: Prisma.NestedUuidNullableWithAggregatesFilter<$PrismaModel> | string | null
|
||||||
|
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedStringNullableFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedStringNullableFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EnumRecurringBookingGroupStatusFilter<$PrismaModel = never> = {
|
||||||
|
equals?: $Enums.RecurringBookingGroupStatus | Prisma.EnumRecurringBookingGroupStatusFieldRefInput<$PrismaModel>
|
||||||
|
in?: $Enums.RecurringBookingGroupStatus[] | Prisma.ListEnumRecurringBookingGroupStatusFieldRefInput<$PrismaModel>
|
||||||
|
notIn?: $Enums.RecurringBookingGroupStatus[] | Prisma.ListEnumRecurringBookingGroupStatusFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedEnumRecurringBookingGroupStatusFilter<$PrismaModel> | $Enums.RecurringBookingGroupStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EnumRecurringBookingGroupStatusWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: $Enums.RecurringBookingGroupStatus | Prisma.EnumRecurringBookingGroupStatusFieldRefInput<$PrismaModel>
|
||||||
|
in?: $Enums.RecurringBookingGroupStatus[] | Prisma.ListEnumRecurringBookingGroupStatusFieldRefInput<$PrismaModel>
|
||||||
|
notIn?: $Enums.RecurringBookingGroupStatus[] | Prisma.ListEnumRecurringBookingGroupStatusFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedEnumRecurringBookingGroupStatusWithAggregatesFilter<$PrismaModel> | $Enums.RecurringBookingGroupStatus
|
||||||
|
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedEnumRecurringBookingGroupStatusFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedEnumRecurringBookingGroupStatusFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
export type JsonFilter<$PrismaModel = never> =
|
export type JsonFilter<$PrismaModel = never> =
|
||||||
| Prisma.PatchUndefined<
|
| Prisma.PatchUndefined<
|
||||||
Prisma.Either<Required<JsonFilterBase<$PrismaModel>>, Exclude<keyof Required<JsonFilterBase<$PrismaModel>>, 'path'>>,
|
Prisma.Either<Required<JsonFilterBase<$PrismaModel>>, Exclude<keyof Required<JsonFilterBase<$PrismaModel>>, 'path'>>,
|
||||||
@@ -381,6 +452,17 @@ export type NestedStringNullableFilter<$PrismaModel = never> = {
|
|||||||
not?: Prisma.NestedStringNullableFilter<$PrismaModel> | string | null
|
not?: Prisma.NestedStringNullableFilter<$PrismaModel> | string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type NestedDateTimeNullableFilter<$PrismaModel = never> = {
|
||||||
|
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
|
||||||
|
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
|
||||||
|
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
|
||||||
|
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedDateTimeNullableFilter<$PrismaModel> | Date | string | null
|
||||||
|
}
|
||||||
|
|
||||||
export type NestedDateTimeFilter<$PrismaModel = never> = {
|
export type NestedDateTimeFilter<$PrismaModel = never> = {
|
||||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
|
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
|
||||||
@@ -456,6 +538,20 @@ export type NestedIntNullableFilter<$PrismaModel = never> = {
|
|||||||
not?: Prisma.NestedIntNullableFilter<$PrismaModel> | number | null
|
not?: Prisma.NestedIntNullableFilter<$PrismaModel> | number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type NestedDateTimeNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
|
||||||
|
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
|
||||||
|
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
|
||||||
|
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedDateTimeNullableWithAggregatesFilter<$PrismaModel> | Date | string | null
|
||||||
|
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
export type NestedDateTimeWithAggregatesFilter<$PrismaModel = never> = {
|
export type NestedDateTimeWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
|
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
|
||||||
@@ -470,29 +566,31 @@ export type NestedDateTimeWithAggregatesFilter<$PrismaModel = never> = {
|
|||||||
_max?: Prisma.NestedDateTimeFilter<$PrismaModel>
|
_max?: Prisma.NestedDateTimeFilter<$PrismaModel>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type NestedDateTimeNullableFilter<$PrismaModel = never> = {
|
export type NestedFloatNullableFilter<$PrismaModel = never> = {
|
||||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
|
equals?: number | Prisma.FloatFieldRefInput<$PrismaModel> | null
|
||||||
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
|
in?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel> | null
|
||||||
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
|
notIn?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel> | null
|
||||||
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
lt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||||
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
lte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||||
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
gt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||||
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
gte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||||
not?: Prisma.NestedDateTimeNullableFilter<$PrismaModel> | Date | string | null
|
not?: Prisma.NestedFloatNullableFilter<$PrismaModel> | number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export type NestedDateTimeNullableWithAggregatesFilter<$PrismaModel = never> = {
|
export type NestedFloatNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
|
equals?: number | Prisma.FloatFieldRefInput<$PrismaModel> | null
|
||||||
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
|
in?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel> | null
|
||||||
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
|
notIn?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel> | null
|
||||||
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
lt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||||
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
lte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||||
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
gt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||||
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
gte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||||
not?: Prisma.NestedDateTimeNullableWithAggregatesFilter<$PrismaModel> | Date | string | null
|
not?: Prisma.NestedFloatNullableWithAggregatesFilter<$PrismaModel> | number | null
|
||||||
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||||
_min?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
|
_avg?: Prisma.NestedFloatNullableFilter<$PrismaModel>
|
||||||
_max?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
|
_sum?: Prisma.NestedFloatNullableFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedFloatNullableFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedFloatNullableFilter<$PrismaModel>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type NestedUuidFilter<$PrismaModel = never> = {
|
export type NestedUuidFilter<$PrismaModel = never> = {
|
||||||
@@ -632,6 +730,17 @@ export type NestedEnumCourtBookingStatusFilter<$PrismaModel = never> = {
|
|||||||
not?: Prisma.NestedEnumCourtBookingStatusFilter<$PrismaModel> | $Enums.CourtBookingStatus
|
not?: Prisma.NestedEnumCourtBookingStatusFilter<$PrismaModel> | $Enums.CourtBookingStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type NestedUuidNullableFilter<$PrismaModel = never> = {
|
||||||
|
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
|
||||||
|
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
|
||||||
|
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
|
||||||
|
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedUuidNullableFilter<$PrismaModel> | string | null
|
||||||
|
}
|
||||||
|
|
||||||
export type NestedEnumCourtBookingStatusWithAggregatesFilter<$PrismaModel = never> = {
|
export type NestedEnumCourtBookingStatusWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
equals?: $Enums.CourtBookingStatus | Prisma.EnumCourtBookingStatusFieldRefInput<$PrismaModel>
|
equals?: $Enums.CourtBookingStatus | Prisma.EnumCourtBookingStatusFieldRefInput<$PrismaModel>
|
||||||
in?: $Enums.CourtBookingStatus[] | Prisma.ListEnumCourtBookingStatusFieldRefInput<$PrismaModel>
|
in?: $Enums.CourtBookingStatus[] | Prisma.ListEnumCourtBookingStatusFieldRefInput<$PrismaModel>
|
||||||
@@ -642,6 +751,37 @@ export type NestedEnumCourtBookingStatusWithAggregatesFilter<$PrismaModel = neve
|
|||||||
_max?: Prisma.NestedEnumCourtBookingStatusFilter<$PrismaModel>
|
_max?: Prisma.NestedEnumCourtBookingStatusFilter<$PrismaModel>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type NestedUuidNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
|
||||||
|
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
|
||||||
|
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
|
||||||
|
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedUuidNullableWithAggregatesFilter<$PrismaModel> | string | null
|
||||||
|
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedStringNullableFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedStringNullableFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NestedEnumRecurringBookingGroupStatusFilter<$PrismaModel = never> = {
|
||||||
|
equals?: $Enums.RecurringBookingGroupStatus | Prisma.EnumRecurringBookingGroupStatusFieldRefInput<$PrismaModel>
|
||||||
|
in?: $Enums.RecurringBookingGroupStatus[] | Prisma.ListEnumRecurringBookingGroupStatusFieldRefInput<$PrismaModel>
|
||||||
|
notIn?: $Enums.RecurringBookingGroupStatus[] | Prisma.ListEnumRecurringBookingGroupStatusFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedEnumRecurringBookingGroupStatusFilter<$PrismaModel> | $Enums.RecurringBookingGroupStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NestedEnumRecurringBookingGroupStatusWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: $Enums.RecurringBookingGroupStatus | Prisma.EnumRecurringBookingGroupStatusFieldRefInput<$PrismaModel>
|
||||||
|
in?: $Enums.RecurringBookingGroupStatus[] | Prisma.ListEnumRecurringBookingGroupStatusFieldRefInput<$PrismaModel>
|
||||||
|
notIn?: $Enums.RecurringBookingGroupStatus[] | Prisma.ListEnumRecurringBookingGroupStatusFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedEnumRecurringBookingGroupStatusWithAggregatesFilter<$PrismaModel> | $Enums.RecurringBookingGroupStatus
|
||||||
|
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedEnumRecurringBookingGroupStatusFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedEnumRecurringBookingGroupStatusFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
export type NestedJsonFilter<$PrismaModel = never> =
|
export type NestedJsonFilter<$PrismaModel = never> =
|
||||||
| Prisma.PatchUndefined<
|
| Prisma.PatchUndefined<
|
||||||
Prisma.Either<Required<NestedJsonFilterBase<$PrismaModel>>, Exclude<keyof Required<NestedJsonFilterBase<$PrismaModel>>, 'path'>>,
|
Prisma.Either<Required<NestedJsonFilterBase<$PrismaModel>>, Exclude<keyof Required<NestedJsonFilterBase<$PrismaModel>>, 'path'>>,
|
||||||
|
|||||||
@@ -38,3 +38,11 @@ export const CourtBookingStatus = {
|
|||||||
} as const
|
} as const
|
||||||
|
|
||||||
export type CourtBookingStatus = (typeof CourtBookingStatus)[keyof typeof CourtBookingStatus]
|
export type CourtBookingStatus = (typeof CourtBookingStatus)[keyof typeof CourtBookingStatus]
|
||||||
|
|
||||||
|
|
||||||
|
export const RecurringBookingGroupStatus = {
|
||||||
|
ACTIVE: 'ACTIVE',
|
||||||
|
CANCELLED: 'CANCELLED'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type RecurringBookingGroupStatus = (typeof RecurringBookingGroupStatus)[keyof typeof RecurringBookingGroupStatus]
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -393,11 +393,12 @@ export const ModelName = {
|
|||||||
ComplexInvitation: 'ComplexInvitation',
|
ComplexInvitation: 'ComplexInvitation',
|
||||||
Sport: 'Sport',
|
Sport: 'Sport',
|
||||||
Court: 'Court',
|
Court: 'Court',
|
||||||
|
CourtMaintenance: 'CourtMaintenance',
|
||||||
CourtAvailability: 'CourtAvailability',
|
CourtAvailability: 'CourtAvailability',
|
||||||
CourtPriceRule: 'CourtPriceRule',
|
CourtPriceRule: 'CourtPriceRule',
|
||||||
CourtBooking: 'CourtBooking',
|
CourtBooking: 'CourtBooking',
|
||||||
CourtBookingLog: 'CourtBookingLog',
|
CourtBookingLog: 'CourtBookingLog',
|
||||||
OnboardingRequest: 'OnboardingRequest',
|
RecurringBookingGroup: 'RecurringBookingGroup',
|
||||||
PasswordResetRequest: 'PasswordResetRequest',
|
PasswordResetRequest: 'PasswordResetRequest',
|
||||||
Plan: 'Plan'
|
Plan: 'Plan'
|
||||||
} as const
|
} as const
|
||||||
@@ -415,7 +416,7 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
|
|||||||
omit: GlobalOmitOptions
|
omit: GlobalOmitOptions
|
||||||
}
|
}
|
||||||
meta: {
|
meta: {
|
||||||
modelProps: "user" | "session" | "account" | "verification" | "complex" | "complexUser" | "complexInvitation" | "sport" | "court" | "courtAvailability" | "courtPriceRule" | "courtBooking" | "courtBookingLog" | "onboardingRequest" | "passwordResetRequest" | "plan"
|
modelProps: "user" | "session" | "account" | "verification" | "complex" | "complexUser" | "complexInvitation" | "sport" | "court" | "courtMaintenance" | "courtAvailability" | "courtPriceRule" | "courtBooking" | "courtBookingLog" | "recurringBookingGroup" | "passwordResetRequest" | "plan"
|
||||||
txIsolationLevel: TransactionIsolationLevel
|
txIsolationLevel: TransactionIsolationLevel
|
||||||
}
|
}
|
||||||
model: {
|
model: {
|
||||||
@@ -1085,6 +1086,80 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
CourtMaintenance: {
|
||||||
|
payload: Prisma.$CourtMaintenancePayload<ExtArgs>
|
||||||
|
fields: Prisma.CourtMaintenanceFieldRefs
|
||||||
|
operations: {
|
||||||
|
findUnique: {
|
||||||
|
args: Prisma.CourtMaintenanceFindUniqueArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$CourtMaintenancePayload> | null
|
||||||
|
}
|
||||||
|
findUniqueOrThrow: {
|
||||||
|
args: Prisma.CourtMaintenanceFindUniqueOrThrowArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$CourtMaintenancePayload>
|
||||||
|
}
|
||||||
|
findFirst: {
|
||||||
|
args: Prisma.CourtMaintenanceFindFirstArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$CourtMaintenancePayload> | null
|
||||||
|
}
|
||||||
|
findFirstOrThrow: {
|
||||||
|
args: Prisma.CourtMaintenanceFindFirstOrThrowArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$CourtMaintenancePayload>
|
||||||
|
}
|
||||||
|
findMany: {
|
||||||
|
args: Prisma.CourtMaintenanceFindManyArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$CourtMaintenancePayload>[]
|
||||||
|
}
|
||||||
|
create: {
|
||||||
|
args: Prisma.CourtMaintenanceCreateArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$CourtMaintenancePayload>
|
||||||
|
}
|
||||||
|
createMany: {
|
||||||
|
args: Prisma.CourtMaintenanceCreateManyArgs<ExtArgs>
|
||||||
|
result: BatchPayload
|
||||||
|
}
|
||||||
|
createManyAndReturn: {
|
||||||
|
args: Prisma.CourtMaintenanceCreateManyAndReturnArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$CourtMaintenancePayload>[]
|
||||||
|
}
|
||||||
|
delete: {
|
||||||
|
args: Prisma.CourtMaintenanceDeleteArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$CourtMaintenancePayload>
|
||||||
|
}
|
||||||
|
update: {
|
||||||
|
args: Prisma.CourtMaintenanceUpdateArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$CourtMaintenancePayload>
|
||||||
|
}
|
||||||
|
deleteMany: {
|
||||||
|
args: Prisma.CourtMaintenanceDeleteManyArgs<ExtArgs>
|
||||||
|
result: BatchPayload
|
||||||
|
}
|
||||||
|
updateMany: {
|
||||||
|
args: Prisma.CourtMaintenanceUpdateManyArgs<ExtArgs>
|
||||||
|
result: BatchPayload
|
||||||
|
}
|
||||||
|
updateManyAndReturn: {
|
||||||
|
args: Prisma.CourtMaintenanceUpdateManyAndReturnArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$CourtMaintenancePayload>[]
|
||||||
|
}
|
||||||
|
upsert: {
|
||||||
|
args: Prisma.CourtMaintenanceUpsertArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$CourtMaintenancePayload>
|
||||||
|
}
|
||||||
|
aggregate: {
|
||||||
|
args: Prisma.CourtMaintenanceAggregateArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.Optional<Prisma.AggregateCourtMaintenance>
|
||||||
|
}
|
||||||
|
groupBy: {
|
||||||
|
args: Prisma.CourtMaintenanceGroupByArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.Optional<Prisma.CourtMaintenanceGroupByOutputType>[]
|
||||||
|
}
|
||||||
|
count: {
|
||||||
|
args: Prisma.CourtMaintenanceCountArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.Optional<Prisma.CourtMaintenanceCountAggregateOutputType> | number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
CourtAvailability: {
|
CourtAvailability: {
|
||||||
payload: Prisma.$CourtAvailabilityPayload<ExtArgs>
|
payload: Prisma.$CourtAvailabilityPayload<ExtArgs>
|
||||||
fields: Prisma.CourtAvailabilityFieldRefs
|
fields: Prisma.CourtAvailabilityFieldRefs
|
||||||
@@ -1381,77 +1456,77 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
OnboardingRequest: {
|
RecurringBookingGroup: {
|
||||||
payload: Prisma.$OnboardingRequestPayload<ExtArgs>
|
payload: Prisma.$RecurringBookingGroupPayload<ExtArgs>
|
||||||
fields: Prisma.OnboardingRequestFieldRefs
|
fields: Prisma.RecurringBookingGroupFieldRefs
|
||||||
operations: {
|
operations: {
|
||||||
findUnique: {
|
findUnique: {
|
||||||
args: Prisma.OnboardingRequestFindUniqueArgs<ExtArgs>
|
args: Prisma.RecurringBookingGroupFindUniqueArgs<ExtArgs>
|
||||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$OnboardingRequestPayload> | null
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$RecurringBookingGroupPayload> | null
|
||||||
}
|
}
|
||||||
findUniqueOrThrow: {
|
findUniqueOrThrow: {
|
||||||
args: Prisma.OnboardingRequestFindUniqueOrThrowArgs<ExtArgs>
|
args: Prisma.RecurringBookingGroupFindUniqueOrThrowArgs<ExtArgs>
|
||||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$OnboardingRequestPayload>
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$RecurringBookingGroupPayload>
|
||||||
}
|
}
|
||||||
findFirst: {
|
findFirst: {
|
||||||
args: Prisma.OnboardingRequestFindFirstArgs<ExtArgs>
|
args: Prisma.RecurringBookingGroupFindFirstArgs<ExtArgs>
|
||||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$OnboardingRequestPayload> | null
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$RecurringBookingGroupPayload> | null
|
||||||
}
|
}
|
||||||
findFirstOrThrow: {
|
findFirstOrThrow: {
|
||||||
args: Prisma.OnboardingRequestFindFirstOrThrowArgs<ExtArgs>
|
args: Prisma.RecurringBookingGroupFindFirstOrThrowArgs<ExtArgs>
|
||||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$OnboardingRequestPayload>
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$RecurringBookingGroupPayload>
|
||||||
}
|
}
|
||||||
findMany: {
|
findMany: {
|
||||||
args: Prisma.OnboardingRequestFindManyArgs<ExtArgs>
|
args: Prisma.RecurringBookingGroupFindManyArgs<ExtArgs>
|
||||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$OnboardingRequestPayload>[]
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$RecurringBookingGroupPayload>[]
|
||||||
}
|
}
|
||||||
create: {
|
create: {
|
||||||
args: Prisma.OnboardingRequestCreateArgs<ExtArgs>
|
args: Prisma.RecurringBookingGroupCreateArgs<ExtArgs>
|
||||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$OnboardingRequestPayload>
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$RecurringBookingGroupPayload>
|
||||||
}
|
}
|
||||||
createMany: {
|
createMany: {
|
||||||
args: Prisma.OnboardingRequestCreateManyArgs<ExtArgs>
|
args: Prisma.RecurringBookingGroupCreateManyArgs<ExtArgs>
|
||||||
result: BatchPayload
|
result: BatchPayload
|
||||||
}
|
}
|
||||||
createManyAndReturn: {
|
createManyAndReturn: {
|
||||||
args: Prisma.OnboardingRequestCreateManyAndReturnArgs<ExtArgs>
|
args: Prisma.RecurringBookingGroupCreateManyAndReturnArgs<ExtArgs>
|
||||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$OnboardingRequestPayload>[]
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$RecurringBookingGroupPayload>[]
|
||||||
}
|
}
|
||||||
delete: {
|
delete: {
|
||||||
args: Prisma.OnboardingRequestDeleteArgs<ExtArgs>
|
args: Prisma.RecurringBookingGroupDeleteArgs<ExtArgs>
|
||||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$OnboardingRequestPayload>
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$RecurringBookingGroupPayload>
|
||||||
}
|
}
|
||||||
update: {
|
update: {
|
||||||
args: Prisma.OnboardingRequestUpdateArgs<ExtArgs>
|
args: Prisma.RecurringBookingGroupUpdateArgs<ExtArgs>
|
||||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$OnboardingRequestPayload>
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$RecurringBookingGroupPayload>
|
||||||
}
|
}
|
||||||
deleteMany: {
|
deleteMany: {
|
||||||
args: Prisma.OnboardingRequestDeleteManyArgs<ExtArgs>
|
args: Prisma.RecurringBookingGroupDeleteManyArgs<ExtArgs>
|
||||||
result: BatchPayload
|
result: BatchPayload
|
||||||
}
|
}
|
||||||
updateMany: {
|
updateMany: {
|
||||||
args: Prisma.OnboardingRequestUpdateManyArgs<ExtArgs>
|
args: Prisma.RecurringBookingGroupUpdateManyArgs<ExtArgs>
|
||||||
result: BatchPayload
|
result: BatchPayload
|
||||||
}
|
}
|
||||||
updateManyAndReturn: {
|
updateManyAndReturn: {
|
||||||
args: Prisma.OnboardingRequestUpdateManyAndReturnArgs<ExtArgs>
|
args: Prisma.RecurringBookingGroupUpdateManyAndReturnArgs<ExtArgs>
|
||||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$OnboardingRequestPayload>[]
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$RecurringBookingGroupPayload>[]
|
||||||
}
|
}
|
||||||
upsert: {
|
upsert: {
|
||||||
args: Prisma.OnboardingRequestUpsertArgs<ExtArgs>
|
args: Prisma.RecurringBookingGroupUpsertArgs<ExtArgs>
|
||||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$OnboardingRequestPayload>
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$RecurringBookingGroupPayload>
|
||||||
}
|
}
|
||||||
aggregate: {
|
aggregate: {
|
||||||
args: Prisma.OnboardingRequestAggregateArgs<ExtArgs>
|
args: Prisma.RecurringBookingGroupAggregateArgs<ExtArgs>
|
||||||
result: runtime.Types.Utils.Optional<Prisma.AggregateOnboardingRequest>
|
result: runtime.Types.Utils.Optional<Prisma.AggregateRecurringBookingGroup>
|
||||||
}
|
}
|
||||||
groupBy: {
|
groupBy: {
|
||||||
args: Prisma.OnboardingRequestGroupByArgs<ExtArgs>
|
args: Prisma.RecurringBookingGroupGroupByArgs<ExtArgs>
|
||||||
result: runtime.Types.Utils.Optional<Prisma.OnboardingRequestGroupByOutputType>[]
|
result: runtime.Types.Utils.Optional<Prisma.RecurringBookingGroupGroupByOutputType>[]
|
||||||
}
|
}
|
||||||
count: {
|
count: {
|
||||||
args: Prisma.OnboardingRequestCountArgs<ExtArgs>
|
args: Prisma.RecurringBookingGroupCountArgs<ExtArgs>
|
||||||
result: runtime.Types.Utils.Optional<Prisma.OnboardingRequestCountAggregateOutputType> | number
|
result: runtime.Types.Utils.Optional<Prisma.RecurringBookingGroupCountAggregateOutputType> | number
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1649,6 +1724,9 @@ export const UserScalarFieldEnum = {
|
|||||||
emailVerified: 'emailVerified',
|
emailVerified: 'emailVerified',
|
||||||
image: 'image',
|
image: 'image',
|
||||||
phone: 'phone',
|
phone: 'phone',
|
||||||
|
banned: 'banned',
|
||||||
|
bannedAt: 'bannedAt',
|
||||||
|
banReason: 'banReason',
|
||||||
createdAt: 'createdAt',
|
createdAt: 'createdAt',
|
||||||
updatedAt: 'updatedAt',
|
updatedAt: 'updatedAt',
|
||||||
role: 'role'
|
role: 'role'
|
||||||
@@ -1665,6 +1743,11 @@ export const SessionScalarFieldEnum = {
|
|||||||
updatedAt: 'updatedAt',
|
updatedAt: 'updatedAt',
|
||||||
ipAddress: 'ipAddress',
|
ipAddress: 'ipAddress',
|
||||||
userAgent: 'userAgent',
|
userAgent: 'userAgent',
|
||||||
|
country: 'country',
|
||||||
|
city: 'city',
|
||||||
|
countryCode: 'countryCode',
|
||||||
|
latitude: 'latitude',
|
||||||
|
longitude: 'longitude',
|
||||||
userId: 'userId'
|
userId: 'userId'
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
@@ -1763,6 +1846,8 @@ export const CourtScalarFieldEnum = {
|
|||||||
name: 'name',
|
name: 'name',
|
||||||
slotDurationMinutes: 'slotDurationMinutes',
|
slotDurationMinutes: 'slotDurationMinutes',
|
||||||
basePrice: 'basePrice',
|
basePrice: 'basePrice',
|
||||||
|
isUnderMaintenance: 'isUnderMaintenance',
|
||||||
|
maintenanceReason: 'maintenanceReason',
|
||||||
createdAt: 'createdAt',
|
createdAt: 'createdAt',
|
||||||
updatedAt: 'updatedAt'
|
updatedAt: 'updatedAt'
|
||||||
} as const
|
} as const
|
||||||
@@ -1770,6 +1855,20 @@ export const CourtScalarFieldEnum = {
|
|||||||
export type CourtScalarFieldEnum = (typeof CourtScalarFieldEnum)[keyof typeof CourtScalarFieldEnum]
|
export type CourtScalarFieldEnum = (typeof CourtScalarFieldEnum)[keyof typeof CourtScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
export const CourtMaintenanceScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
courtId: 'courtId',
|
||||||
|
startDate: 'startDate',
|
||||||
|
startTime: 'startTime',
|
||||||
|
endTime: 'endTime',
|
||||||
|
reason: 'reason',
|
||||||
|
createdAt: 'createdAt',
|
||||||
|
updatedAt: 'updatedAt'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type CourtMaintenanceScalarFieldEnum = (typeof CourtMaintenanceScalarFieldEnum)[keyof typeof CourtMaintenanceScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
export const CourtAvailabilityScalarFieldEnum = {
|
export const CourtAvailabilityScalarFieldEnum = {
|
||||||
id: 'id',
|
id: 'id',
|
||||||
courtId: 'courtId',
|
courtId: 'courtId',
|
||||||
@@ -1806,7 +1905,9 @@ export const CourtBookingScalarFieldEnum = {
|
|||||||
endTime: 'endTime',
|
endTime: 'endTime',
|
||||||
customerName: 'customerName',
|
customerName: 'customerName',
|
||||||
customerPhone: 'customerPhone',
|
customerPhone: 'customerPhone',
|
||||||
|
customerEmail: 'customerEmail',
|
||||||
status: 'status',
|
status: 'status',
|
||||||
|
recurringGroupId: 'recurringGroupId',
|
||||||
createdAt: 'createdAt',
|
createdAt: 'createdAt',
|
||||||
updatedAt: 'updatedAt'
|
updatedAt: 'updatedAt'
|
||||||
} as const
|
} as const
|
||||||
@@ -1825,28 +1926,31 @@ export const CourtBookingLogScalarFieldEnum = {
|
|||||||
newStatus: 'newStatus',
|
newStatus: 'newStatus',
|
||||||
customerName: 'customerName',
|
customerName: 'customerName',
|
||||||
customerPhone: 'customerPhone',
|
customerPhone: 'customerPhone',
|
||||||
|
customerEmail: 'customerEmail',
|
||||||
changedAt: 'changedAt'
|
changedAt: 'changedAt'
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export type CourtBookingLogScalarFieldEnum = (typeof CourtBookingLogScalarFieldEnum)[keyof typeof CourtBookingLogScalarFieldEnum]
|
export type CourtBookingLogScalarFieldEnum = (typeof CourtBookingLogScalarFieldEnum)[keyof typeof CourtBookingLogScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
export const OnboardingRequestScalarFieldEnum = {
|
export const RecurringBookingGroupScalarFieldEnum = {
|
||||||
id: 'id',
|
id: 'id',
|
||||||
fullName: 'fullName',
|
complexId: 'complexId',
|
||||||
email: 'email',
|
courtId: 'courtId',
|
||||||
otpHash: 'otpHash',
|
startTime: 'startTime',
|
||||||
otpExpiresAt: 'otpExpiresAt',
|
endTime: 'endTime',
|
||||||
otpAttempts: 'otpAttempts',
|
dayOfWeek: 'dayOfWeek',
|
||||||
otpLastSentAt: 'otpLastSentAt',
|
startDate: 'startDate',
|
||||||
otpResendCount: 'otpResendCount',
|
endDate: 'endDate',
|
||||||
emailVerifiedAt: 'emailVerifiedAt',
|
status: 'status',
|
||||||
completedAt: 'completedAt',
|
customerName: 'customerName',
|
||||||
|
customerPhone: 'customerPhone',
|
||||||
|
customerEmail: 'customerEmail',
|
||||||
createdAt: 'createdAt',
|
createdAt: 'createdAt',
|
||||||
updatedAt: 'updatedAt'
|
updatedAt: 'updatedAt'
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export type OnboardingRequestScalarFieldEnum = (typeof OnboardingRequestScalarFieldEnum)[keyof typeof OnboardingRequestScalarFieldEnum]
|
export type RecurringBookingGroupScalarFieldEnum = (typeof RecurringBookingGroupScalarFieldEnum)[keyof typeof RecurringBookingGroupScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
export const PasswordResetRequestScalarFieldEnum = {
|
export const PasswordResetRequestScalarFieldEnum = {
|
||||||
@@ -1957,6 +2061,20 @@ export type ListDateTimeFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaM
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reference to a field of type 'Float'
|
||||||
|
*/
|
||||||
|
export type FloatFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'Float'>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reference to a field of type 'Float[]'
|
||||||
|
*/
|
||||||
|
export type ListFloatFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'Float[]'>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reference to a field of type 'ComplexUserRole'
|
* Reference to a field of type 'ComplexUserRole'
|
||||||
*/
|
*/
|
||||||
@@ -2027,6 +2145,20 @@ export type ListEnumCourtBookingStatusFieldRefInput<$PrismaModel> = FieldRefInpu
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reference to a field of type 'RecurringBookingGroupStatus'
|
||||||
|
*/
|
||||||
|
export type EnumRecurringBookingGroupStatusFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'RecurringBookingGroupStatus'>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reference to a field of type 'RecurringBookingGroupStatus[]'
|
||||||
|
*/
|
||||||
|
export type ListEnumRecurringBookingGroupStatusFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'RecurringBookingGroupStatus[]'>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reference to a field of type 'Json'
|
* Reference to a field of type 'Json'
|
||||||
*/
|
*/
|
||||||
@@ -2040,20 +2172,6 @@ export type JsonFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'J
|
|||||||
export type EnumQueryModeFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'QueryMode'>
|
export type EnumQueryModeFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'QueryMode'>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reference to a field of type 'Float'
|
|
||||||
*/
|
|
||||||
export type FloatFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'Float'>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reference to a field of type 'Float[]'
|
|
||||||
*/
|
|
||||||
export type ListFloatFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'Float[]'>
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Batch Payload for updateMany & deleteMany & createMany
|
* Batch Payload for updateMany & deleteMany & createMany
|
||||||
*/
|
*/
|
||||||
@@ -2158,11 +2276,12 @@ export type GlobalOmitConfig = {
|
|||||||
complexInvitation?: Prisma.ComplexInvitationOmit
|
complexInvitation?: Prisma.ComplexInvitationOmit
|
||||||
sport?: Prisma.SportOmit
|
sport?: Prisma.SportOmit
|
||||||
court?: Prisma.CourtOmit
|
court?: Prisma.CourtOmit
|
||||||
|
courtMaintenance?: Prisma.CourtMaintenanceOmit
|
||||||
courtAvailability?: Prisma.CourtAvailabilityOmit
|
courtAvailability?: Prisma.CourtAvailabilityOmit
|
||||||
courtPriceRule?: Prisma.CourtPriceRuleOmit
|
courtPriceRule?: Prisma.CourtPriceRuleOmit
|
||||||
courtBooking?: Prisma.CourtBookingOmit
|
courtBooking?: Prisma.CourtBookingOmit
|
||||||
courtBookingLog?: Prisma.CourtBookingLogOmit
|
courtBookingLog?: Prisma.CourtBookingLogOmit
|
||||||
onboardingRequest?: Prisma.OnboardingRequestOmit
|
recurringBookingGroup?: Prisma.RecurringBookingGroupOmit
|
||||||
passwordResetRequest?: Prisma.PasswordResetRequestOmit
|
passwordResetRequest?: Prisma.PasswordResetRequestOmit
|
||||||
plan?: Prisma.PlanOmit
|
plan?: Prisma.PlanOmit
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,11 +60,12 @@ export const ModelName = {
|
|||||||
ComplexInvitation: 'ComplexInvitation',
|
ComplexInvitation: 'ComplexInvitation',
|
||||||
Sport: 'Sport',
|
Sport: 'Sport',
|
||||||
Court: 'Court',
|
Court: 'Court',
|
||||||
|
CourtMaintenance: 'CourtMaintenance',
|
||||||
CourtAvailability: 'CourtAvailability',
|
CourtAvailability: 'CourtAvailability',
|
||||||
CourtPriceRule: 'CourtPriceRule',
|
CourtPriceRule: 'CourtPriceRule',
|
||||||
CourtBooking: 'CourtBooking',
|
CourtBooking: 'CourtBooking',
|
||||||
CourtBookingLog: 'CourtBookingLog',
|
CourtBookingLog: 'CourtBookingLog',
|
||||||
OnboardingRequest: 'OnboardingRequest',
|
RecurringBookingGroup: 'RecurringBookingGroup',
|
||||||
PasswordResetRequest: 'PasswordResetRequest',
|
PasswordResetRequest: 'PasswordResetRequest',
|
||||||
Plan: 'Plan'
|
Plan: 'Plan'
|
||||||
} as const
|
} as const
|
||||||
@@ -92,6 +93,9 @@ export const UserScalarFieldEnum = {
|
|||||||
emailVerified: 'emailVerified',
|
emailVerified: 'emailVerified',
|
||||||
image: 'image',
|
image: 'image',
|
||||||
phone: 'phone',
|
phone: 'phone',
|
||||||
|
banned: 'banned',
|
||||||
|
bannedAt: 'bannedAt',
|
||||||
|
banReason: 'banReason',
|
||||||
createdAt: 'createdAt',
|
createdAt: 'createdAt',
|
||||||
updatedAt: 'updatedAt',
|
updatedAt: 'updatedAt',
|
||||||
role: 'role'
|
role: 'role'
|
||||||
@@ -108,6 +112,11 @@ export const SessionScalarFieldEnum = {
|
|||||||
updatedAt: 'updatedAt',
|
updatedAt: 'updatedAt',
|
||||||
ipAddress: 'ipAddress',
|
ipAddress: 'ipAddress',
|
||||||
userAgent: 'userAgent',
|
userAgent: 'userAgent',
|
||||||
|
country: 'country',
|
||||||
|
city: 'city',
|
||||||
|
countryCode: 'countryCode',
|
||||||
|
latitude: 'latitude',
|
||||||
|
longitude: 'longitude',
|
||||||
userId: 'userId'
|
userId: 'userId'
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
@@ -206,6 +215,8 @@ export const CourtScalarFieldEnum = {
|
|||||||
name: 'name',
|
name: 'name',
|
||||||
slotDurationMinutes: 'slotDurationMinutes',
|
slotDurationMinutes: 'slotDurationMinutes',
|
||||||
basePrice: 'basePrice',
|
basePrice: 'basePrice',
|
||||||
|
isUnderMaintenance: 'isUnderMaintenance',
|
||||||
|
maintenanceReason: 'maintenanceReason',
|
||||||
createdAt: 'createdAt',
|
createdAt: 'createdAt',
|
||||||
updatedAt: 'updatedAt'
|
updatedAt: 'updatedAt'
|
||||||
} as const
|
} as const
|
||||||
@@ -213,6 +224,20 @@ export const CourtScalarFieldEnum = {
|
|||||||
export type CourtScalarFieldEnum = (typeof CourtScalarFieldEnum)[keyof typeof CourtScalarFieldEnum]
|
export type CourtScalarFieldEnum = (typeof CourtScalarFieldEnum)[keyof typeof CourtScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
export const CourtMaintenanceScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
courtId: 'courtId',
|
||||||
|
startDate: 'startDate',
|
||||||
|
startTime: 'startTime',
|
||||||
|
endTime: 'endTime',
|
||||||
|
reason: 'reason',
|
||||||
|
createdAt: 'createdAt',
|
||||||
|
updatedAt: 'updatedAt'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type CourtMaintenanceScalarFieldEnum = (typeof CourtMaintenanceScalarFieldEnum)[keyof typeof CourtMaintenanceScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
export const CourtAvailabilityScalarFieldEnum = {
|
export const CourtAvailabilityScalarFieldEnum = {
|
||||||
id: 'id',
|
id: 'id',
|
||||||
courtId: 'courtId',
|
courtId: 'courtId',
|
||||||
@@ -249,7 +274,9 @@ export const CourtBookingScalarFieldEnum = {
|
|||||||
endTime: 'endTime',
|
endTime: 'endTime',
|
||||||
customerName: 'customerName',
|
customerName: 'customerName',
|
||||||
customerPhone: 'customerPhone',
|
customerPhone: 'customerPhone',
|
||||||
|
customerEmail: 'customerEmail',
|
||||||
status: 'status',
|
status: 'status',
|
||||||
|
recurringGroupId: 'recurringGroupId',
|
||||||
createdAt: 'createdAt',
|
createdAt: 'createdAt',
|
||||||
updatedAt: 'updatedAt'
|
updatedAt: 'updatedAt'
|
||||||
} as const
|
} as const
|
||||||
@@ -268,28 +295,31 @@ export const CourtBookingLogScalarFieldEnum = {
|
|||||||
newStatus: 'newStatus',
|
newStatus: 'newStatus',
|
||||||
customerName: 'customerName',
|
customerName: 'customerName',
|
||||||
customerPhone: 'customerPhone',
|
customerPhone: 'customerPhone',
|
||||||
|
customerEmail: 'customerEmail',
|
||||||
changedAt: 'changedAt'
|
changedAt: 'changedAt'
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export type CourtBookingLogScalarFieldEnum = (typeof CourtBookingLogScalarFieldEnum)[keyof typeof CourtBookingLogScalarFieldEnum]
|
export type CourtBookingLogScalarFieldEnum = (typeof CourtBookingLogScalarFieldEnum)[keyof typeof CourtBookingLogScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
export const OnboardingRequestScalarFieldEnum = {
|
export const RecurringBookingGroupScalarFieldEnum = {
|
||||||
id: 'id',
|
id: 'id',
|
||||||
fullName: 'fullName',
|
complexId: 'complexId',
|
||||||
email: 'email',
|
courtId: 'courtId',
|
||||||
otpHash: 'otpHash',
|
startTime: 'startTime',
|
||||||
otpExpiresAt: 'otpExpiresAt',
|
endTime: 'endTime',
|
||||||
otpAttempts: 'otpAttempts',
|
dayOfWeek: 'dayOfWeek',
|
||||||
otpLastSentAt: 'otpLastSentAt',
|
startDate: 'startDate',
|
||||||
otpResendCount: 'otpResendCount',
|
endDate: 'endDate',
|
||||||
emailVerifiedAt: 'emailVerifiedAt',
|
status: 'status',
|
||||||
completedAt: 'completedAt',
|
customerName: 'customerName',
|
||||||
|
customerPhone: 'customerPhone',
|
||||||
|
customerEmail: 'customerEmail',
|
||||||
createdAt: 'createdAt',
|
createdAt: 'createdAt',
|
||||||
updatedAt: 'updatedAt'
|
updatedAt: 'updatedAt'
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export type OnboardingRequestScalarFieldEnum = (typeof OnboardingRequestScalarFieldEnum)[keyof typeof OnboardingRequestScalarFieldEnum]
|
export type RecurringBookingGroupScalarFieldEnum = (typeof RecurringBookingGroupScalarFieldEnum)[keyof typeof RecurringBookingGroupScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
export const PasswordResetRequestScalarFieldEnum = {
|
export const PasswordResetRequestScalarFieldEnum = {
|
||||||
|
|||||||
@@ -17,11 +17,12 @@ export type * from './models/ComplexUser'
|
|||||||
export type * from './models/ComplexInvitation'
|
export type * from './models/ComplexInvitation'
|
||||||
export type * from './models/Sport'
|
export type * from './models/Sport'
|
||||||
export type * from './models/Court'
|
export type * from './models/Court'
|
||||||
|
export type * from './models/CourtMaintenance'
|
||||||
export type * from './models/CourtAvailability'
|
export type * from './models/CourtAvailability'
|
||||||
export type * from './models/CourtPriceRule'
|
export type * from './models/CourtPriceRule'
|
||||||
export type * from './models/CourtBooking'
|
export type * from './models/CourtBooking'
|
||||||
export type * from './models/CourtBookingLog'
|
export type * from './models/CourtBookingLog'
|
||||||
export type * from './models/OnboardingRequest'
|
export type * from './models/RecurringBookingGroup'
|
||||||
export type * from './models/PasswordResetRequest'
|
export type * from './models/PasswordResetRequest'
|
||||||
export type * from './models/Plan'
|
export type * from './models/Plan'
|
||||||
export type * from './commonInputTypes'
|
export type * from './commonInputTypes'
|
||||||
@@ -234,6 +234,7 @@ export type ComplexWhereInput = {
|
|||||||
users?: Prisma.ComplexUserListRelationFilter
|
users?: Prisma.ComplexUserListRelationFilter
|
||||||
invitations?: Prisma.ComplexInvitationListRelationFilter
|
invitations?: Prisma.ComplexInvitationListRelationFilter
|
||||||
courts?: Prisma.CourtListRelationFilter
|
courts?: Prisma.CourtListRelationFilter
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupListRelationFilter
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ComplexOrderByWithRelationInput = {
|
export type ComplexOrderByWithRelationInput = {
|
||||||
@@ -252,6 +253,7 @@ export type ComplexOrderByWithRelationInput = {
|
|||||||
users?: Prisma.ComplexUserOrderByRelationAggregateInput
|
users?: Prisma.ComplexUserOrderByRelationAggregateInput
|
||||||
invitations?: Prisma.ComplexInvitationOrderByRelationAggregateInput
|
invitations?: Prisma.ComplexInvitationOrderByRelationAggregateInput
|
||||||
courts?: Prisma.CourtOrderByRelationAggregateInput
|
courts?: Prisma.CourtOrderByRelationAggregateInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupOrderByRelationAggregateInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ComplexWhereUniqueInput = Prisma.AtLeast<{
|
export type ComplexWhereUniqueInput = Prisma.AtLeast<{
|
||||||
@@ -273,6 +275,7 @@ export type ComplexWhereUniqueInput = Prisma.AtLeast<{
|
|||||||
users?: Prisma.ComplexUserListRelationFilter
|
users?: Prisma.ComplexUserListRelationFilter
|
||||||
invitations?: Prisma.ComplexInvitationListRelationFilter
|
invitations?: Prisma.ComplexInvitationListRelationFilter
|
||||||
courts?: Prisma.CourtListRelationFilter
|
courts?: Prisma.CourtListRelationFilter
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupListRelationFilter
|
||||||
}, "id" | "complexSlug">
|
}, "id" | "complexSlug">
|
||||||
|
|
||||||
export type ComplexOrderByWithAggregationInput = {
|
export type ComplexOrderByWithAggregationInput = {
|
||||||
@@ -324,6 +327,7 @@ export type ComplexCreateInput = {
|
|||||||
users?: Prisma.ComplexUserCreateNestedManyWithoutComplexInput
|
users?: Prisma.ComplexUserCreateNestedManyWithoutComplexInput
|
||||||
invitations?: Prisma.ComplexInvitationCreateNestedManyWithoutComplexInput
|
invitations?: Prisma.ComplexInvitationCreateNestedManyWithoutComplexInput
|
||||||
courts?: Prisma.CourtCreateNestedManyWithoutComplexInput
|
courts?: Prisma.CourtCreateNestedManyWithoutComplexInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupCreateNestedManyWithoutComplexInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ComplexUncheckedCreateInput = {
|
export type ComplexUncheckedCreateInput = {
|
||||||
@@ -341,6 +345,7 @@ export type ComplexUncheckedCreateInput = {
|
|||||||
users?: Prisma.ComplexUserUncheckedCreateNestedManyWithoutComplexInput
|
users?: Prisma.ComplexUserUncheckedCreateNestedManyWithoutComplexInput
|
||||||
invitations?: Prisma.ComplexInvitationUncheckedCreateNestedManyWithoutComplexInput
|
invitations?: Prisma.ComplexInvitationUncheckedCreateNestedManyWithoutComplexInput
|
||||||
courts?: Prisma.CourtUncheckedCreateNestedManyWithoutComplexInput
|
courts?: Prisma.CourtUncheckedCreateNestedManyWithoutComplexInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupUncheckedCreateNestedManyWithoutComplexInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ComplexUpdateInput = {
|
export type ComplexUpdateInput = {
|
||||||
@@ -358,6 +363,7 @@ export type ComplexUpdateInput = {
|
|||||||
users?: Prisma.ComplexUserUpdateManyWithoutComplexNestedInput
|
users?: Prisma.ComplexUserUpdateManyWithoutComplexNestedInput
|
||||||
invitations?: Prisma.ComplexInvitationUpdateManyWithoutComplexNestedInput
|
invitations?: Prisma.ComplexInvitationUpdateManyWithoutComplexNestedInput
|
||||||
courts?: Prisma.CourtUpdateManyWithoutComplexNestedInput
|
courts?: Prisma.CourtUpdateManyWithoutComplexNestedInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupUpdateManyWithoutComplexNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ComplexUncheckedUpdateInput = {
|
export type ComplexUncheckedUpdateInput = {
|
||||||
@@ -375,6 +381,7 @@ export type ComplexUncheckedUpdateInput = {
|
|||||||
users?: Prisma.ComplexUserUncheckedUpdateManyWithoutComplexNestedInput
|
users?: Prisma.ComplexUserUncheckedUpdateManyWithoutComplexNestedInput
|
||||||
invitations?: Prisma.ComplexInvitationUncheckedUpdateManyWithoutComplexNestedInput
|
invitations?: Prisma.ComplexInvitationUncheckedUpdateManyWithoutComplexNestedInput
|
||||||
courts?: Prisma.CourtUncheckedUpdateManyWithoutComplexNestedInput
|
courts?: Prisma.CourtUncheckedUpdateManyWithoutComplexNestedInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupUncheckedUpdateManyWithoutComplexNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ComplexCreateManyInput = {
|
export type ComplexCreateManyInput = {
|
||||||
@@ -517,6 +524,20 @@ export type ComplexUpdateOneRequiredWithoutCourtsNestedInput = {
|
|||||||
update?: Prisma.XOR<Prisma.XOR<Prisma.ComplexUpdateToOneWithWhereWithoutCourtsInput, Prisma.ComplexUpdateWithoutCourtsInput>, Prisma.ComplexUncheckedUpdateWithoutCourtsInput>
|
update?: Prisma.XOR<Prisma.XOR<Prisma.ComplexUpdateToOneWithWhereWithoutCourtsInput, Prisma.ComplexUpdateWithoutCourtsInput>, Prisma.ComplexUncheckedUpdateWithoutCourtsInput>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ComplexCreateNestedOneWithoutRecurringGroupsInput = {
|
||||||
|
create?: Prisma.XOR<Prisma.ComplexCreateWithoutRecurringGroupsInput, Prisma.ComplexUncheckedCreateWithoutRecurringGroupsInput>
|
||||||
|
connectOrCreate?: Prisma.ComplexCreateOrConnectWithoutRecurringGroupsInput
|
||||||
|
connect?: Prisma.ComplexWhereUniqueInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ComplexUpdateOneRequiredWithoutRecurringGroupsNestedInput = {
|
||||||
|
create?: Prisma.XOR<Prisma.ComplexCreateWithoutRecurringGroupsInput, Prisma.ComplexUncheckedCreateWithoutRecurringGroupsInput>
|
||||||
|
connectOrCreate?: Prisma.ComplexCreateOrConnectWithoutRecurringGroupsInput
|
||||||
|
upsert?: Prisma.ComplexUpsertWithoutRecurringGroupsInput
|
||||||
|
connect?: Prisma.ComplexWhereUniqueInput
|
||||||
|
update?: Prisma.XOR<Prisma.XOR<Prisma.ComplexUpdateToOneWithWhereWithoutRecurringGroupsInput, Prisma.ComplexUpdateWithoutRecurringGroupsInput>, Prisma.ComplexUncheckedUpdateWithoutRecurringGroupsInput>
|
||||||
|
}
|
||||||
|
|
||||||
export type ComplexCreateNestedManyWithoutPlanInput = {
|
export type ComplexCreateNestedManyWithoutPlanInput = {
|
||||||
create?: Prisma.XOR<Prisma.ComplexCreateWithoutPlanInput, Prisma.ComplexUncheckedCreateWithoutPlanInput> | Prisma.ComplexCreateWithoutPlanInput[] | Prisma.ComplexUncheckedCreateWithoutPlanInput[]
|
create?: Prisma.XOR<Prisma.ComplexCreateWithoutPlanInput, Prisma.ComplexUncheckedCreateWithoutPlanInput> | Prisma.ComplexCreateWithoutPlanInput[] | Prisma.ComplexUncheckedCreateWithoutPlanInput[]
|
||||||
connectOrCreate?: Prisma.ComplexCreateOrConnectWithoutPlanInput | Prisma.ComplexCreateOrConnectWithoutPlanInput[]
|
connectOrCreate?: Prisma.ComplexCreateOrConnectWithoutPlanInput | Prisma.ComplexCreateOrConnectWithoutPlanInput[]
|
||||||
@@ -573,6 +594,7 @@ export type ComplexCreateWithoutUsersInput = {
|
|||||||
plan?: Prisma.PlanCreateNestedOneWithoutComplexesInput
|
plan?: Prisma.PlanCreateNestedOneWithoutComplexesInput
|
||||||
invitations?: Prisma.ComplexInvitationCreateNestedManyWithoutComplexInput
|
invitations?: Prisma.ComplexInvitationCreateNestedManyWithoutComplexInput
|
||||||
courts?: Prisma.CourtCreateNestedManyWithoutComplexInput
|
courts?: Prisma.CourtCreateNestedManyWithoutComplexInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupCreateNestedManyWithoutComplexInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ComplexUncheckedCreateWithoutUsersInput = {
|
export type ComplexUncheckedCreateWithoutUsersInput = {
|
||||||
@@ -589,6 +611,7 @@ export type ComplexUncheckedCreateWithoutUsersInput = {
|
|||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
invitations?: Prisma.ComplexInvitationUncheckedCreateNestedManyWithoutComplexInput
|
invitations?: Prisma.ComplexInvitationUncheckedCreateNestedManyWithoutComplexInput
|
||||||
courts?: Prisma.CourtUncheckedCreateNestedManyWithoutComplexInput
|
courts?: Prisma.CourtUncheckedCreateNestedManyWithoutComplexInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupUncheckedCreateNestedManyWithoutComplexInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ComplexCreateOrConnectWithoutUsersInput = {
|
export type ComplexCreateOrConnectWithoutUsersInput = {
|
||||||
@@ -621,6 +644,7 @@ export type ComplexUpdateWithoutUsersInput = {
|
|||||||
plan?: Prisma.PlanUpdateOneWithoutComplexesNestedInput
|
plan?: Prisma.PlanUpdateOneWithoutComplexesNestedInput
|
||||||
invitations?: Prisma.ComplexInvitationUpdateManyWithoutComplexNestedInput
|
invitations?: Prisma.ComplexInvitationUpdateManyWithoutComplexNestedInput
|
||||||
courts?: Prisma.CourtUpdateManyWithoutComplexNestedInput
|
courts?: Prisma.CourtUpdateManyWithoutComplexNestedInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupUpdateManyWithoutComplexNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ComplexUncheckedUpdateWithoutUsersInput = {
|
export type ComplexUncheckedUpdateWithoutUsersInput = {
|
||||||
@@ -637,6 +661,7 @@ export type ComplexUncheckedUpdateWithoutUsersInput = {
|
|||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
invitations?: Prisma.ComplexInvitationUncheckedUpdateManyWithoutComplexNestedInput
|
invitations?: Prisma.ComplexInvitationUncheckedUpdateManyWithoutComplexNestedInput
|
||||||
courts?: Prisma.CourtUncheckedUpdateManyWithoutComplexNestedInput
|
courts?: Prisma.CourtUncheckedUpdateManyWithoutComplexNestedInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupUncheckedUpdateManyWithoutComplexNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ComplexCreateWithoutInvitationsInput = {
|
export type ComplexCreateWithoutInvitationsInput = {
|
||||||
@@ -653,6 +678,7 @@ export type ComplexCreateWithoutInvitationsInput = {
|
|||||||
plan?: Prisma.PlanCreateNestedOneWithoutComplexesInput
|
plan?: Prisma.PlanCreateNestedOneWithoutComplexesInput
|
||||||
users?: Prisma.ComplexUserCreateNestedManyWithoutComplexInput
|
users?: Prisma.ComplexUserCreateNestedManyWithoutComplexInput
|
||||||
courts?: Prisma.CourtCreateNestedManyWithoutComplexInput
|
courts?: Prisma.CourtCreateNestedManyWithoutComplexInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupCreateNestedManyWithoutComplexInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ComplexUncheckedCreateWithoutInvitationsInput = {
|
export type ComplexUncheckedCreateWithoutInvitationsInput = {
|
||||||
@@ -669,6 +695,7 @@ export type ComplexUncheckedCreateWithoutInvitationsInput = {
|
|||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
users?: Prisma.ComplexUserUncheckedCreateNestedManyWithoutComplexInput
|
users?: Prisma.ComplexUserUncheckedCreateNestedManyWithoutComplexInput
|
||||||
courts?: Prisma.CourtUncheckedCreateNestedManyWithoutComplexInput
|
courts?: Prisma.CourtUncheckedCreateNestedManyWithoutComplexInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupUncheckedCreateNestedManyWithoutComplexInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ComplexCreateOrConnectWithoutInvitationsInput = {
|
export type ComplexCreateOrConnectWithoutInvitationsInput = {
|
||||||
@@ -701,6 +728,7 @@ export type ComplexUpdateWithoutInvitationsInput = {
|
|||||||
plan?: Prisma.PlanUpdateOneWithoutComplexesNestedInput
|
plan?: Prisma.PlanUpdateOneWithoutComplexesNestedInput
|
||||||
users?: Prisma.ComplexUserUpdateManyWithoutComplexNestedInput
|
users?: Prisma.ComplexUserUpdateManyWithoutComplexNestedInput
|
||||||
courts?: Prisma.CourtUpdateManyWithoutComplexNestedInput
|
courts?: Prisma.CourtUpdateManyWithoutComplexNestedInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupUpdateManyWithoutComplexNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ComplexUncheckedUpdateWithoutInvitationsInput = {
|
export type ComplexUncheckedUpdateWithoutInvitationsInput = {
|
||||||
@@ -717,6 +745,7 @@ export type ComplexUncheckedUpdateWithoutInvitationsInput = {
|
|||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
users?: Prisma.ComplexUserUncheckedUpdateManyWithoutComplexNestedInput
|
users?: Prisma.ComplexUserUncheckedUpdateManyWithoutComplexNestedInput
|
||||||
courts?: Prisma.CourtUncheckedUpdateManyWithoutComplexNestedInput
|
courts?: Prisma.CourtUncheckedUpdateManyWithoutComplexNestedInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupUncheckedUpdateManyWithoutComplexNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ComplexCreateWithoutCourtsInput = {
|
export type ComplexCreateWithoutCourtsInput = {
|
||||||
@@ -733,6 +762,7 @@ export type ComplexCreateWithoutCourtsInput = {
|
|||||||
plan?: Prisma.PlanCreateNestedOneWithoutComplexesInput
|
plan?: Prisma.PlanCreateNestedOneWithoutComplexesInput
|
||||||
users?: Prisma.ComplexUserCreateNestedManyWithoutComplexInput
|
users?: Prisma.ComplexUserCreateNestedManyWithoutComplexInput
|
||||||
invitations?: Prisma.ComplexInvitationCreateNestedManyWithoutComplexInput
|
invitations?: Prisma.ComplexInvitationCreateNestedManyWithoutComplexInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupCreateNestedManyWithoutComplexInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ComplexUncheckedCreateWithoutCourtsInput = {
|
export type ComplexUncheckedCreateWithoutCourtsInput = {
|
||||||
@@ -749,6 +779,7 @@ export type ComplexUncheckedCreateWithoutCourtsInput = {
|
|||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
users?: Prisma.ComplexUserUncheckedCreateNestedManyWithoutComplexInput
|
users?: Prisma.ComplexUserUncheckedCreateNestedManyWithoutComplexInput
|
||||||
invitations?: Prisma.ComplexInvitationUncheckedCreateNestedManyWithoutComplexInput
|
invitations?: Prisma.ComplexInvitationUncheckedCreateNestedManyWithoutComplexInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupUncheckedCreateNestedManyWithoutComplexInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ComplexCreateOrConnectWithoutCourtsInput = {
|
export type ComplexCreateOrConnectWithoutCourtsInput = {
|
||||||
@@ -781,6 +812,7 @@ export type ComplexUpdateWithoutCourtsInput = {
|
|||||||
plan?: Prisma.PlanUpdateOneWithoutComplexesNestedInput
|
plan?: Prisma.PlanUpdateOneWithoutComplexesNestedInput
|
||||||
users?: Prisma.ComplexUserUpdateManyWithoutComplexNestedInput
|
users?: Prisma.ComplexUserUpdateManyWithoutComplexNestedInput
|
||||||
invitations?: Prisma.ComplexInvitationUpdateManyWithoutComplexNestedInput
|
invitations?: Prisma.ComplexInvitationUpdateManyWithoutComplexNestedInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupUpdateManyWithoutComplexNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ComplexUncheckedUpdateWithoutCourtsInput = {
|
export type ComplexUncheckedUpdateWithoutCourtsInput = {
|
||||||
@@ -797,6 +829,91 @@ export type ComplexUncheckedUpdateWithoutCourtsInput = {
|
|||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
users?: Prisma.ComplexUserUncheckedUpdateManyWithoutComplexNestedInput
|
users?: Prisma.ComplexUserUncheckedUpdateManyWithoutComplexNestedInput
|
||||||
invitations?: Prisma.ComplexInvitationUncheckedUpdateManyWithoutComplexNestedInput
|
invitations?: Prisma.ComplexInvitationUncheckedUpdateManyWithoutComplexNestedInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupUncheckedUpdateManyWithoutComplexNestedInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ComplexCreateWithoutRecurringGroupsInput = {
|
||||||
|
id: string
|
||||||
|
complexName: string
|
||||||
|
physicalAddress?: string | null
|
||||||
|
city?: string | null
|
||||||
|
state?: string | null
|
||||||
|
country?: string | null
|
||||||
|
complexSlug: string
|
||||||
|
adminEmail: string
|
||||||
|
createdAt?: Date | string
|
||||||
|
updatedAt?: Date | string
|
||||||
|
plan?: Prisma.PlanCreateNestedOneWithoutComplexesInput
|
||||||
|
users?: Prisma.ComplexUserCreateNestedManyWithoutComplexInput
|
||||||
|
invitations?: Prisma.ComplexInvitationCreateNestedManyWithoutComplexInput
|
||||||
|
courts?: Prisma.CourtCreateNestedManyWithoutComplexInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ComplexUncheckedCreateWithoutRecurringGroupsInput = {
|
||||||
|
id: string
|
||||||
|
complexName: string
|
||||||
|
physicalAddress?: string | null
|
||||||
|
city?: string | null
|
||||||
|
state?: string | null
|
||||||
|
country?: string | null
|
||||||
|
complexSlug: string
|
||||||
|
adminEmail: string
|
||||||
|
planCode?: string | null
|
||||||
|
createdAt?: Date | string
|
||||||
|
updatedAt?: Date | string
|
||||||
|
users?: Prisma.ComplexUserUncheckedCreateNestedManyWithoutComplexInput
|
||||||
|
invitations?: Prisma.ComplexInvitationUncheckedCreateNestedManyWithoutComplexInput
|
||||||
|
courts?: Prisma.CourtUncheckedCreateNestedManyWithoutComplexInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ComplexCreateOrConnectWithoutRecurringGroupsInput = {
|
||||||
|
where: Prisma.ComplexWhereUniqueInput
|
||||||
|
create: Prisma.XOR<Prisma.ComplexCreateWithoutRecurringGroupsInput, Prisma.ComplexUncheckedCreateWithoutRecurringGroupsInput>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ComplexUpsertWithoutRecurringGroupsInput = {
|
||||||
|
update: Prisma.XOR<Prisma.ComplexUpdateWithoutRecurringGroupsInput, Prisma.ComplexUncheckedUpdateWithoutRecurringGroupsInput>
|
||||||
|
create: Prisma.XOR<Prisma.ComplexCreateWithoutRecurringGroupsInput, Prisma.ComplexUncheckedCreateWithoutRecurringGroupsInput>
|
||||||
|
where?: Prisma.ComplexWhereInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ComplexUpdateToOneWithWhereWithoutRecurringGroupsInput = {
|
||||||
|
where?: Prisma.ComplexWhereInput
|
||||||
|
data: Prisma.XOR<Prisma.ComplexUpdateWithoutRecurringGroupsInput, Prisma.ComplexUncheckedUpdateWithoutRecurringGroupsInput>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ComplexUpdateWithoutRecurringGroupsInput = {
|
||||||
|
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
complexName?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
physicalAddress?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
city?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
state?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
country?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
complexSlug?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
adminEmail?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
plan?: Prisma.PlanUpdateOneWithoutComplexesNestedInput
|
||||||
|
users?: Prisma.ComplexUserUpdateManyWithoutComplexNestedInput
|
||||||
|
invitations?: Prisma.ComplexInvitationUpdateManyWithoutComplexNestedInput
|
||||||
|
courts?: Prisma.CourtUpdateManyWithoutComplexNestedInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ComplexUncheckedUpdateWithoutRecurringGroupsInput = {
|
||||||
|
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
complexName?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
physicalAddress?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
city?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
state?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
country?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
complexSlug?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
adminEmail?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
planCode?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
users?: Prisma.ComplexUserUncheckedUpdateManyWithoutComplexNestedInput
|
||||||
|
invitations?: Prisma.ComplexInvitationUncheckedUpdateManyWithoutComplexNestedInput
|
||||||
|
courts?: Prisma.CourtUncheckedUpdateManyWithoutComplexNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ComplexCreateWithoutPlanInput = {
|
export type ComplexCreateWithoutPlanInput = {
|
||||||
@@ -813,6 +930,7 @@ export type ComplexCreateWithoutPlanInput = {
|
|||||||
users?: Prisma.ComplexUserCreateNestedManyWithoutComplexInput
|
users?: Prisma.ComplexUserCreateNestedManyWithoutComplexInput
|
||||||
invitations?: Prisma.ComplexInvitationCreateNestedManyWithoutComplexInput
|
invitations?: Prisma.ComplexInvitationCreateNestedManyWithoutComplexInput
|
||||||
courts?: Prisma.CourtCreateNestedManyWithoutComplexInput
|
courts?: Prisma.CourtCreateNestedManyWithoutComplexInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupCreateNestedManyWithoutComplexInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ComplexUncheckedCreateWithoutPlanInput = {
|
export type ComplexUncheckedCreateWithoutPlanInput = {
|
||||||
@@ -829,6 +947,7 @@ export type ComplexUncheckedCreateWithoutPlanInput = {
|
|||||||
users?: Prisma.ComplexUserUncheckedCreateNestedManyWithoutComplexInput
|
users?: Prisma.ComplexUserUncheckedCreateNestedManyWithoutComplexInput
|
||||||
invitations?: Prisma.ComplexInvitationUncheckedCreateNestedManyWithoutComplexInput
|
invitations?: Prisma.ComplexInvitationUncheckedCreateNestedManyWithoutComplexInput
|
||||||
courts?: Prisma.CourtUncheckedCreateNestedManyWithoutComplexInput
|
courts?: Prisma.CourtUncheckedCreateNestedManyWithoutComplexInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupUncheckedCreateNestedManyWithoutComplexInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ComplexCreateOrConnectWithoutPlanInput = {
|
export type ComplexCreateOrConnectWithoutPlanInput = {
|
||||||
@@ -901,6 +1020,7 @@ export type ComplexUpdateWithoutPlanInput = {
|
|||||||
users?: Prisma.ComplexUserUpdateManyWithoutComplexNestedInput
|
users?: Prisma.ComplexUserUpdateManyWithoutComplexNestedInput
|
||||||
invitations?: Prisma.ComplexInvitationUpdateManyWithoutComplexNestedInput
|
invitations?: Prisma.ComplexInvitationUpdateManyWithoutComplexNestedInput
|
||||||
courts?: Prisma.CourtUpdateManyWithoutComplexNestedInput
|
courts?: Prisma.CourtUpdateManyWithoutComplexNestedInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupUpdateManyWithoutComplexNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ComplexUncheckedUpdateWithoutPlanInput = {
|
export type ComplexUncheckedUpdateWithoutPlanInput = {
|
||||||
@@ -917,6 +1037,7 @@ export type ComplexUncheckedUpdateWithoutPlanInput = {
|
|||||||
users?: Prisma.ComplexUserUncheckedUpdateManyWithoutComplexNestedInput
|
users?: Prisma.ComplexUserUncheckedUpdateManyWithoutComplexNestedInput
|
||||||
invitations?: Prisma.ComplexInvitationUncheckedUpdateManyWithoutComplexNestedInput
|
invitations?: Prisma.ComplexInvitationUncheckedUpdateManyWithoutComplexNestedInput
|
||||||
courts?: Prisma.CourtUncheckedUpdateManyWithoutComplexNestedInput
|
courts?: Prisma.CourtUncheckedUpdateManyWithoutComplexNestedInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupUncheckedUpdateManyWithoutComplexNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ComplexUncheckedUpdateManyWithoutPlanInput = {
|
export type ComplexUncheckedUpdateManyWithoutPlanInput = {
|
||||||
@@ -941,12 +1062,14 @@ export type ComplexCountOutputType = {
|
|||||||
users: number
|
users: number
|
||||||
invitations: number
|
invitations: number
|
||||||
courts: number
|
courts: number
|
||||||
|
recurringGroups: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ComplexCountOutputTypeSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
export type ComplexCountOutputTypeSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
users?: boolean | ComplexCountOutputTypeCountUsersArgs
|
users?: boolean | ComplexCountOutputTypeCountUsersArgs
|
||||||
invitations?: boolean | ComplexCountOutputTypeCountInvitationsArgs
|
invitations?: boolean | ComplexCountOutputTypeCountInvitationsArgs
|
||||||
courts?: boolean | ComplexCountOutputTypeCountCourtsArgs
|
courts?: boolean | ComplexCountOutputTypeCountCourtsArgs
|
||||||
|
recurringGroups?: boolean | ComplexCountOutputTypeCountRecurringGroupsArgs
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -980,6 +1103,13 @@ export type ComplexCountOutputTypeCountCourtsArgs<ExtArgs extends runtime.Types.
|
|||||||
where?: Prisma.CourtWhereInput
|
where?: Prisma.CourtWhereInput
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ComplexCountOutputType without action
|
||||||
|
*/
|
||||||
|
export type ComplexCountOutputTypeCountRecurringGroupsArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
|
where?: Prisma.RecurringBookingGroupWhereInput
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export type ComplexSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
|
export type ComplexSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
|
||||||
id?: boolean
|
id?: boolean
|
||||||
@@ -997,6 +1127,7 @@ export type ComplexSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs
|
|||||||
users?: boolean | Prisma.Complex$usersArgs<ExtArgs>
|
users?: boolean | Prisma.Complex$usersArgs<ExtArgs>
|
||||||
invitations?: boolean | Prisma.Complex$invitationsArgs<ExtArgs>
|
invitations?: boolean | Prisma.Complex$invitationsArgs<ExtArgs>
|
||||||
courts?: boolean | Prisma.Complex$courtsArgs<ExtArgs>
|
courts?: boolean | Prisma.Complex$courtsArgs<ExtArgs>
|
||||||
|
recurringGroups?: boolean | Prisma.Complex$recurringGroupsArgs<ExtArgs>
|
||||||
_count?: boolean | Prisma.ComplexCountOutputTypeDefaultArgs<ExtArgs>
|
_count?: boolean | Prisma.ComplexCountOutputTypeDefaultArgs<ExtArgs>
|
||||||
}, ExtArgs["result"]["complex"]>
|
}, ExtArgs["result"]["complex"]>
|
||||||
|
|
||||||
@@ -1050,6 +1181,7 @@ export type ComplexInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs
|
|||||||
users?: boolean | Prisma.Complex$usersArgs<ExtArgs>
|
users?: boolean | Prisma.Complex$usersArgs<ExtArgs>
|
||||||
invitations?: boolean | Prisma.Complex$invitationsArgs<ExtArgs>
|
invitations?: boolean | Prisma.Complex$invitationsArgs<ExtArgs>
|
||||||
courts?: boolean | Prisma.Complex$courtsArgs<ExtArgs>
|
courts?: boolean | Prisma.Complex$courtsArgs<ExtArgs>
|
||||||
|
recurringGroups?: boolean | Prisma.Complex$recurringGroupsArgs<ExtArgs>
|
||||||
_count?: boolean | Prisma.ComplexCountOutputTypeDefaultArgs<ExtArgs>
|
_count?: boolean | Prisma.ComplexCountOutputTypeDefaultArgs<ExtArgs>
|
||||||
}
|
}
|
||||||
export type ComplexIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
export type ComplexIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
@@ -1066,6 +1198,7 @@ export type $ComplexPayload<ExtArgs extends runtime.Types.Extensions.InternalArg
|
|||||||
users: Prisma.$ComplexUserPayload<ExtArgs>[]
|
users: Prisma.$ComplexUserPayload<ExtArgs>[]
|
||||||
invitations: Prisma.$ComplexInvitationPayload<ExtArgs>[]
|
invitations: Prisma.$ComplexInvitationPayload<ExtArgs>[]
|
||||||
courts: Prisma.$CourtPayload<ExtArgs>[]
|
courts: Prisma.$CourtPayload<ExtArgs>[]
|
||||||
|
recurringGroups: Prisma.$RecurringBookingGroupPayload<ExtArgs>[]
|
||||||
}
|
}
|
||||||
scalars: runtime.Types.Extensions.GetPayloadResult<{
|
scalars: runtime.Types.Extensions.GetPayloadResult<{
|
||||||
id: string
|
id: string
|
||||||
@@ -1477,6 +1610,7 @@ export interface Prisma__ComplexClient<T, Null = never, ExtArgs extends runtime.
|
|||||||
users<T extends Prisma.Complex$usersArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Complex$usersArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$ComplexUserPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
users<T extends Prisma.Complex$usersArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Complex$usersArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$ComplexUserPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
||||||
invitations<T extends Prisma.Complex$invitationsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Complex$invitationsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$ComplexInvitationPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
invitations<T extends Prisma.Complex$invitationsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Complex$invitationsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$ComplexInvitationPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
||||||
courts<T extends Prisma.Complex$courtsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Complex$courtsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$CourtPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
courts<T extends Prisma.Complex$courtsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Complex$courtsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$CourtPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
||||||
|
recurringGroups<T extends Prisma.Complex$recurringGroupsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Complex$recurringGroupsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$RecurringBookingGroupPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
||||||
/**
|
/**
|
||||||
* Attaches callbacks for the resolution and/or rejection of the Promise.
|
* Attaches callbacks for the resolution and/or rejection of the Promise.
|
||||||
* @param onfulfilled The callback to execute when the Promise is resolved.
|
* @param onfulfilled The callback to execute when the Promise is resolved.
|
||||||
@@ -2008,6 +2142,30 @@ export type Complex$courtsArgs<ExtArgs extends runtime.Types.Extensions.Internal
|
|||||||
distinct?: Prisma.CourtScalarFieldEnum | Prisma.CourtScalarFieldEnum[]
|
distinct?: Prisma.CourtScalarFieldEnum | Prisma.CourtScalarFieldEnum[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Complex.recurringGroups
|
||||||
|
*/
|
||||||
|
export type Complex$recurringGroupsArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
|
/**
|
||||||
|
* Select specific fields to fetch from the RecurringBookingGroup
|
||||||
|
*/
|
||||||
|
select?: Prisma.RecurringBookingGroupSelect<ExtArgs> | null
|
||||||
|
/**
|
||||||
|
* Omit specific fields from the RecurringBookingGroup
|
||||||
|
*/
|
||||||
|
omit?: Prisma.RecurringBookingGroupOmit<ExtArgs> | null
|
||||||
|
/**
|
||||||
|
* Choose, which related nodes to fetch as well
|
||||||
|
*/
|
||||||
|
include?: Prisma.RecurringBookingGroupInclude<ExtArgs> | null
|
||||||
|
where?: Prisma.RecurringBookingGroupWhereInput
|
||||||
|
orderBy?: Prisma.RecurringBookingGroupOrderByWithRelationInput | Prisma.RecurringBookingGroupOrderByWithRelationInput[]
|
||||||
|
cursor?: Prisma.RecurringBookingGroupWhereUniqueInput
|
||||||
|
take?: number
|
||||||
|
skip?: number
|
||||||
|
distinct?: Prisma.RecurringBookingGroupScalarFieldEnum | Prisma.RecurringBookingGroupScalarFieldEnum[]
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Complex without action
|
* Complex without action
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ export type CourtMinAggregateOutputType = {
|
|||||||
name: string | null
|
name: string | null
|
||||||
slotDurationMinutes: number | null
|
slotDurationMinutes: number | null
|
||||||
basePrice: runtime.Decimal | null
|
basePrice: runtime.Decimal | null
|
||||||
|
isUnderMaintenance: boolean | null
|
||||||
|
maintenanceReason: string | null
|
||||||
createdAt: Date | null
|
createdAt: Date | null
|
||||||
updatedAt: Date | null
|
updatedAt: Date | null
|
||||||
}
|
}
|
||||||
@@ -54,6 +56,8 @@ export type CourtMaxAggregateOutputType = {
|
|||||||
name: string | null
|
name: string | null
|
||||||
slotDurationMinutes: number | null
|
slotDurationMinutes: number | null
|
||||||
basePrice: runtime.Decimal | null
|
basePrice: runtime.Decimal | null
|
||||||
|
isUnderMaintenance: boolean | null
|
||||||
|
maintenanceReason: string | null
|
||||||
createdAt: Date | null
|
createdAt: Date | null
|
||||||
updatedAt: Date | null
|
updatedAt: Date | null
|
||||||
}
|
}
|
||||||
@@ -65,6 +69,8 @@ export type CourtCountAggregateOutputType = {
|
|||||||
name: number
|
name: number
|
||||||
slotDurationMinutes: number
|
slotDurationMinutes: number
|
||||||
basePrice: number
|
basePrice: number
|
||||||
|
isUnderMaintenance: number
|
||||||
|
maintenanceReason: number
|
||||||
createdAt: number
|
createdAt: number
|
||||||
updatedAt: number
|
updatedAt: number
|
||||||
_all: number
|
_all: number
|
||||||
@@ -88,6 +94,8 @@ export type CourtMinAggregateInputType = {
|
|||||||
name?: true
|
name?: true
|
||||||
slotDurationMinutes?: true
|
slotDurationMinutes?: true
|
||||||
basePrice?: true
|
basePrice?: true
|
||||||
|
isUnderMaintenance?: true
|
||||||
|
maintenanceReason?: true
|
||||||
createdAt?: true
|
createdAt?: true
|
||||||
updatedAt?: true
|
updatedAt?: true
|
||||||
}
|
}
|
||||||
@@ -99,6 +107,8 @@ export type CourtMaxAggregateInputType = {
|
|||||||
name?: true
|
name?: true
|
||||||
slotDurationMinutes?: true
|
slotDurationMinutes?: true
|
||||||
basePrice?: true
|
basePrice?: true
|
||||||
|
isUnderMaintenance?: true
|
||||||
|
maintenanceReason?: true
|
||||||
createdAt?: true
|
createdAt?: true
|
||||||
updatedAt?: true
|
updatedAt?: true
|
||||||
}
|
}
|
||||||
@@ -110,6 +120,8 @@ export type CourtCountAggregateInputType = {
|
|||||||
name?: true
|
name?: true
|
||||||
slotDurationMinutes?: true
|
slotDurationMinutes?: true
|
||||||
basePrice?: true
|
basePrice?: true
|
||||||
|
isUnderMaintenance?: true
|
||||||
|
maintenanceReason?: true
|
||||||
createdAt?: true
|
createdAt?: true
|
||||||
updatedAt?: true
|
updatedAt?: true
|
||||||
_all?: true
|
_all?: true
|
||||||
@@ -208,6 +220,8 @@ export type CourtGroupByOutputType = {
|
|||||||
name: string
|
name: string
|
||||||
slotDurationMinutes: number
|
slotDurationMinutes: number
|
||||||
basePrice: runtime.Decimal
|
basePrice: runtime.Decimal
|
||||||
|
isUnderMaintenance: boolean
|
||||||
|
maintenanceReason: string | null
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
updatedAt: Date
|
updatedAt: Date
|
||||||
_count: CourtCountAggregateOutputType | null
|
_count: CourtCountAggregateOutputType | null
|
||||||
@@ -242,6 +256,8 @@ export type CourtWhereInput = {
|
|||||||
name?: Prisma.StringFilter<"Court"> | string
|
name?: Prisma.StringFilter<"Court"> | string
|
||||||
slotDurationMinutes?: Prisma.IntFilter<"Court"> | number
|
slotDurationMinutes?: Prisma.IntFilter<"Court"> | number
|
||||||
basePrice?: Prisma.DecimalFilter<"Court"> | runtime.Decimal | runtime.DecimalJsLike | number | string
|
basePrice?: Prisma.DecimalFilter<"Court"> | runtime.Decimal | runtime.DecimalJsLike | number | string
|
||||||
|
isUnderMaintenance?: Prisma.BoolFilter<"Court"> | boolean
|
||||||
|
maintenanceReason?: Prisma.StringNullableFilter<"Court"> | string | null
|
||||||
createdAt?: Prisma.DateTimeFilter<"Court"> | Date | string
|
createdAt?: Prisma.DateTimeFilter<"Court"> | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFilter<"Court"> | Date | string
|
updatedAt?: Prisma.DateTimeFilter<"Court"> | Date | string
|
||||||
complex?: Prisma.XOR<Prisma.ComplexScalarRelationFilter, Prisma.ComplexWhereInput>
|
complex?: Prisma.XOR<Prisma.ComplexScalarRelationFilter, Prisma.ComplexWhereInput>
|
||||||
@@ -249,6 +265,8 @@ export type CourtWhereInput = {
|
|||||||
availabilities?: Prisma.CourtAvailabilityListRelationFilter
|
availabilities?: Prisma.CourtAvailabilityListRelationFilter
|
||||||
priceRules?: Prisma.CourtPriceRuleListRelationFilter
|
priceRules?: Prisma.CourtPriceRuleListRelationFilter
|
||||||
bookings?: Prisma.CourtBookingListRelationFilter
|
bookings?: Prisma.CourtBookingListRelationFilter
|
||||||
|
maintenances?: Prisma.CourtMaintenanceListRelationFilter
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupListRelationFilter
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CourtOrderByWithRelationInput = {
|
export type CourtOrderByWithRelationInput = {
|
||||||
@@ -258,6 +276,8 @@ export type CourtOrderByWithRelationInput = {
|
|||||||
name?: Prisma.SortOrder
|
name?: Prisma.SortOrder
|
||||||
slotDurationMinutes?: Prisma.SortOrder
|
slotDurationMinutes?: Prisma.SortOrder
|
||||||
basePrice?: Prisma.SortOrder
|
basePrice?: Prisma.SortOrder
|
||||||
|
isUnderMaintenance?: Prisma.SortOrder
|
||||||
|
maintenanceReason?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
createdAt?: Prisma.SortOrder
|
createdAt?: Prisma.SortOrder
|
||||||
updatedAt?: Prisma.SortOrder
|
updatedAt?: Prisma.SortOrder
|
||||||
complex?: Prisma.ComplexOrderByWithRelationInput
|
complex?: Prisma.ComplexOrderByWithRelationInput
|
||||||
@@ -265,6 +285,8 @@ export type CourtOrderByWithRelationInput = {
|
|||||||
availabilities?: Prisma.CourtAvailabilityOrderByRelationAggregateInput
|
availabilities?: Prisma.CourtAvailabilityOrderByRelationAggregateInput
|
||||||
priceRules?: Prisma.CourtPriceRuleOrderByRelationAggregateInput
|
priceRules?: Prisma.CourtPriceRuleOrderByRelationAggregateInput
|
||||||
bookings?: Prisma.CourtBookingOrderByRelationAggregateInput
|
bookings?: Prisma.CourtBookingOrderByRelationAggregateInput
|
||||||
|
maintenances?: Prisma.CourtMaintenanceOrderByRelationAggregateInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupOrderByRelationAggregateInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CourtWhereUniqueInput = Prisma.AtLeast<{
|
export type CourtWhereUniqueInput = Prisma.AtLeast<{
|
||||||
@@ -277,6 +299,8 @@ export type CourtWhereUniqueInput = Prisma.AtLeast<{
|
|||||||
name?: Prisma.StringFilter<"Court"> | string
|
name?: Prisma.StringFilter<"Court"> | string
|
||||||
slotDurationMinutes?: Prisma.IntFilter<"Court"> | number
|
slotDurationMinutes?: Prisma.IntFilter<"Court"> | number
|
||||||
basePrice?: Prisma.DecimalFilter<"Court"> | runtime.Decimal | runtime.DecimalJsLike | number | string
|
basePrice?: Prisma.DecimalFilter<"Court"> | runtime.Decimal | runtime.DecimalJsLike | number | string
|
||||||
|
isUnderMaintenance?: Prisma.BoolFilter<"Court"> | boolean
|
||||||
|
maintenanceReason?: Prisma.StringNullableFilter<"Court"> | string | null
|
||||||
createdAt?: Prisma.DateTimeFilter<"Court"> | Date | string
|
createdAt?: Prisma.DateTimeFilter<"Court"> | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFilter<"Court"> | Date | string
|
updatedAt?: Prisma.DateTimeFilter<"Court"> | Date | string
|
||||||
complex?: Prisma.XOR<Prisma.ComplexScalarRelationFilter, Prisma.ComplexWhereInput>
|
complex?: Prisma.XOR<Prisma.ComplexScalarRelationFilter, Prisma.ComplexWhereInput>
|
||||||
@@ -284,6 +308,8 @@ export type CourtWhereUniqueInput = Prisma.AtLeast<{
|
|||||||
availabilities?: Prisma.CourtAvailabilityListRelationFilter
|
availabilities?: Prisma.CourtAvailabilityListRelationFilter
|
||||||
priceRules?: Prisma.CourtPriceRuleListRelationFilter
|
priceRules?: Prisma.CourtPriceRuleListRelationFilter
|
||||||
bookings?: Prisma.CourtBookingListRelationFilter
|
bookings?: Prisma.CourtBookingListRelationFilter
|
||||||
|
maintenances?: Prisma.CourtMaintenanceListRelationFilter
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupListRelationFilter
|
||||||
}, "id">
|
}, "id">
|
||||||
|
|
||||||
export type CourtOrderByWithAggregationInput = {
|
export type CourtOrderByWithAggregationInput = {
|
||||||
@@ -293,6 +319,8 @@ export type CourtOrderByWithAggregationInput = {
|
|||||||
name?: Prisma.SortOrder
|
name?: Prisma.SortOrder
|
||||||
slotDurationMinutes?: Prisma.SortOrder
|
slotDurationMinutes?: Prisma.SortOrder
|
||||||
basePrice?: Prisma.SortOrder
|
basePrice?: Prisma.SortOrder
|
||||||
|
isUnderMaintenance?: Prisma.SortOrder
|
||||||
|
maintenanceReason?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
createdAt?: Prisma.SortOrder
|
createdAt?: Prisma.SortOrder
|
||||||
updatedAt?: Prisma.SortOrder
|
updatedAt?: Prisma.SortOrder
|
||||||
_count?: Prisma.CourtCountOrderByAggregateInput
|
_count?: Prisma.CourtCountOrderByAggregateInput
|
||||||
@@ -312,6 +340,8 @@ export type CourtScalarWhereWithAggregatesInput = {
|
|||||||
name?: Prisma.StringWithAggregatesFilter<"Court"> | string
|
name?: Prisma.StringWithAggregatesFilter<"Court"> | string
|
||||||
slotDurationMinutes?: Prisma.IntWithAggregatesFilter<"Court"> | number
|
slotDurationMinutes?: Prisma.IntWithAggregatesFilter<"Court"> | number
|
||||||
basePrice?: Prisma.DecimalWithAggregatesFilter<"Court"> | runtime.Decimal | runtime.DecimalJsLike | number | string
|
basePrice?: Prisma.DecimalWithAggregatesFilter<"Court"> | runtime.Decimal | runtime.DecimalJsLike | number | string
|
||||||
|
isUnderMaintenance?: Prisma.BoolWithAggregatesFilter<"Court"> | boolean
|
||||||
|
maintenanceReason?: Prisma.StringNullableWithAggregatesFilter<"Court"> | string | null
|
||||||
createdAt?: Prisma.DateTimeWithAggregatesFilter<"Court"> | Date | string
|
createdAt?: Prisma.DateTimeWithAggregatesFilter<"Court"> | Date | string
|
||||||
updatedAt?: Prisma.DateTimeWithAggregatesFilter<"Court"> | Date | string
|
updatedAt?: Prisma.DateTimeWithAggregatesFilter<"Court"> | Date | string
|
||||||
}
|
}
|
||||||
@@ -321,6 +351,8 @@ export type CourtCreateInput = {
|
|||||||
name: string
|
name: string
|
||||||
slotDurationMinutes: number
|
slotDurationMinutes: number
|
||||||
basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string
|
basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string
|
||||||
|
isUnderMaintenance?: boolean
|
||||||
|
maintenanceReason?: string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
complex: Prisma.ComplexCreateNestedOneWithoutCourtsInput
|
complex: Prisma.ComplexCreateNestedOneWithoutCourtsInput
|
||||||
@@ -328,6 +360,8 @@ export type CourtCreateInput = {
|
|||||||
availabilities?: Prisma.CourtAvailabilityCreateNestedManyWithoutCourtInput
|
availabilities?: Prisma.CourtAvailabilityCreateNestedManyWithoutCourtInput
|
||||||
priceRules?: Prisma.CourtPriceRuleCreateNestedManyWithoutCourtInput
|
priceRules?: Prisma.CourtPriceRuleCreateNestedManyWithoutCourtInput
|
||||||
bookings?: Prisma.CourtBookingCreateNestedManyWithoutCourtInput
|
bookings?: Prisma.CourtBookingCreateNestedManyWithoutCourtInput
|
||||||
|
maintenances?: Prisma.CourtMaintenanceCreateNestedManyWithoutCourtInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupCreateNestedManyWithoutCourtInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CourtUncheckedCreateInput = {
|
export type CourtUncheckedCreateInput = {
|
||||||
@@ -337,11 +371,15 @@ export type CourtUncheckedCreateInput = {
|
|||||||
name: string
|
name: string
|
||||||
slotDurationMinutes: number
|
slotDurationMinutes: number
|
||||||
basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string
|
basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string
|
||||||
|
isUnderMaintenance?: boolean
|
||||||
|
maintenanceReason?: string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
availabilities?: Prisma.CourtAvailabilityUncheckedCreateNestedManyWithoutCourtInput
|
availabilities?: Prisma.CourtAvailabilityUncheckedCreateNestedManyWithoutCourtInput
|
||||||
priceRules?: Prisma.CourtPriceRuleUncheckedCreateNestedManyWithoutCourtInput
|
priceRules?: Prisma.CourtPriceRuleUncheckedCreateNestedManyWithoutCourtInput
|
||||||
bookings?: Prisma.CourtBookingUncheckedCreateNestedManyWithoutCourtInput
|
bookings?: Prisma.CourtBookingUncheckedCreateNestedManyWithoutCourtInput
|
||||||
|
maintenances?: Prisma.CourtMaintenanceUncheckedCreateNestedManyWithoutCourtInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupUncheckedCreateNestedManyWithoutCourtInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CourtUpdateInput = {
|
export type CourtUpdateInput = {
|
||||||
@@ -349,6 +387,8 @@ export type CourtUpdateInput = {
|
|||||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number
|
slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string
|
basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string
|
||||||
|
isUnderMaintenance?: Prisma.BoolFieldUpdateOperationsInput | boolean
|
||||||
|
maintenanceReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
complex?: Prisma.ComplexUpdateOneRequiredWithoutCourtsNestedInput
|
complex?: Prisma.ComplexUpdateOneRequiredWithoutCourtsNestedInput
|
||||||
@@ -356,6 +396,8 @@ export type CourtUpdateInput = {
|
|||||||
availabilities?: Prisma.CourtAvailabilityUpdateManyWithoutCourtNestedInput
|
availabilities?: Prisma.CourtAvailabilityUpdateManyWithoutCourtNestedInput
|
||||||
priceRules?: Prisma.CourtPriceRuleUpdateManyWithoutCourtNestedInput
|
priceRules?: Prisma.CourtPriceRuleUpdateManyWithoutCourtNestedInput
|
||||||
bookings?: Prisma.CourtBookingUpdateManyWithoutCourtNestedInput
|
bookings?: Prisma.CourtBookingUpdateManyWithoutCourtNestedInput
|
||||||
|
maintenances?: Prisma.CourtMaintenanceUpdateManyWithoutCourtNestedInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupUpdateManyWithoutCourtNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CourtUncheckedUpdateInput = {
|
export type CourtUncheckedUpdateInput = {
|
||||||
@@ -365,11 +407,15 @@ export type CourtUncheckedUpdateInput = {
|
|||||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number
|
slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string
|
basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string
|
||||||
|
isUnderMaintenance?: Prisma.BoolFieldUpdateOperationsInput | boolean
|
||||||
|
maintenanceReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
availabilities?: Prisma.CourtAvailabilityUncheckedUpdateManyWithoutCourtNestedInput
|
availabilities?: Prisma.CourtAvailabilityUncheckedUpdateManyWithoutCourtNestedInput
|
||||||
priceRules?: Prisma.CourtPriceRuleUncheckedUpdateManyWithoutCourtNestedInput
|
priceRules?: Prisma.CourtPriceRuleUncheckedUpdateManyWithoutCourtNestedInput
|
||||||
bookings?: Prisma.CourtBookingUncheckedUpdateManyWithoutCourtNestedInput
|
bookings?: Prisma.CourtBookingUncheckedUpdateManyWithoutCourtNestedInput
|
||||||
|
maintenances?: Prisma.CourtMaintenanceUncheckedUpdateManyWithoutCourtNestedInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupUncheckedUpdateManyWithoutCourtNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CourtCreateManyInput = {
|
export type CourtCreateManyInput = {
|
||||||
@@ -379,6 +425,8 @@ export type CourtCreateManyInput = {
|
|||||||
name: string
|
name: string
|
||||||
slotDurationMinutes: number
|
slotDurationMinutes: number
|
||||||
basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string
|
basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string
|
||||||
|
isUnderMaintenance?: boolean
|
||||||
|
maintenanceReason?: string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
}
|
}
|
||||||
@@ -388,6 +436,8 @@ export type CourtUpdateManyMutationInput = {
|
|||||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number
|
slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string
|
basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string
|
||||||
|
isUnderMaintenance?: Prisma.BoolFieldUpdateOperationsInput | boolean
|
||||||
|
maintenanceReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
}
|
}
|
||||||
@@ -399,6 +449,8 @@ export type CourtUncheckedUpdateManyInput = {
|
|||||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number
|
slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string
|
basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string
|
||||||
|
isUnderMaintenance?: Prisma.BoolFieldUpdateOperationsInput | boolean
|
||||||
|
maintenanceReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
}
|
}
|
||||||
@@ -420,6 +472,8 @@ export type CourtCountOrderByAggregateInput = {
|
|||||||
name?: Prisma.SortOrder
|
name?: Prisma.SortOrder
|
||||||
slotDurationMinutes?: Prisma.SortOrder
|
slotDurationMinutes?: Prisma.SortOrder
|
||||||
basePrice?: Prisma.SortOrder
|
basePrice?: Prisma.SortOrder
|
||||||
|
isUnderMaintenance?: Prisma.SortOrder
|
||||||
|
maintenanceReason?: Prisma.SortOrder
|
||||||
createdAt?: Prisma.SortOrder
|
createdAt?: Prisma.SortOrder
|
||||||
updatedAt?: Prisma.SortOrder
|
updatedAt?: Prisma.SortOrder
|
||||||
}
|
}
|
||||||
@@ -436,6 +490,8 @@ export type CourtMaxOrderByAggregateInput = {
|
|||||||
name?: Prisma.SortOrder
|
name?: Prisma.SortOrder
|
||||||
slotDurationMinutes?: Prisma.SortOrder
|
slotDurationMinutes?: Prisma.SortOrder
|
||||||
basePrice?: Prisma.SortOrder
|
basePrice?: Prisma.SortOrder
|
||||||
|
isUnderMaintenance?: Prisma.SortOrder
|
||||||
|
maintenanceReason?: Prisma.SortOrder
|
||||||
createdAt?: Prisma.SortOrder
|
createdAt?: Prisma.SortOrder
|
||||||
updatedAt?: Prisma.SortOrder
|
updatedAt?: Prisma.SortOrder
|
||||||
}
|
}
|
||||||
@@ -447,6 +503,8 @@ export type CourtMinOrderByAggregateInput = {
|
|||||||
name?: Prisma.SortOrder
|
name?: Prisma.SortOrder
|
||||||
slotDurationMinutes?: Prisma.SortOrder
|
slotDurationMinutes?: Prisma.SortOrder
|
||||||
basePrice?: Prisma.SortOrder
|
basePrice?: Prisma.SortOrder
|
||||||
|
isUnderMaintenance?: Prisma.SortOrder
|
||||||
|
maintenanceReason?: Prisma.SortOrder
|
||||||
createdAt?: Prisma.SortOrder
|
createdAt?: Prisma.SortOrder
|
||||||
updatedAt?: Prisma.SortOrder
|
updatedAt?: Prisma.SortOrder
|
||||||
}
|
}
|
||||||
@@ -561,6 +619,20 @@ export type DecimalFieldUpdateOperationsInput = {
|
|||||||
divide?: runtime.Decimal | runtime.DecimalJsLike | number | string
|
divide?: runtime.Decimal | runtime.DecimalJsLike | number | string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type CourtCreateNestedOneWithoutMaintenancesInput = {
|
||||||
|
create?: Prisma.XOR<Prisma.CourtCreateWithoutMaintenancesInput, Prisma.CourtUncheckedCreateWithoutMaintenancesInput>
|
||||||
|
connectOrCreate?: Prisma.CourtCreateOrConnectWithoutMaintenancesInput
|
||||||
|
connect?: Prisma.CourtWhereUniqueInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CourtUpdateOneRequiredWithoutMaintenancesNestedInput = {
|
||||||
|
create?: Prisma.XOR<Prisma.CourtCreateWithoutMaintenancesInput, Prisma.CourtUncheckedCreateWithoutMaintenancesInput>
|
||||||
|
connectOrCreate?: Prisma.CourtCreateOrConnectWithoutMaintenancesInput
|
||||||
|
upsert?: Prisma.CourtUpsertWithoutMaintenancesInput
|
||||||
|
connect?: Prisma.CourtWhereUniqueInput
|
||||||
|
update?: Prisma.XOR<Prisma.XOR<Prisma.CourtUpdateToOneWithWhereWithoutMaintenancesInput, Prisma.CourtUpdateWithoutMaintenancesInput>, Prisma.CourtUncheckedUpdateWithoutMaintenancesInput>
|
||||||
|
}
|
||||||
|
|
||||||
export type CourtCreateNestedOneWithoutAvailabilitiesInput = {
|
export type CourtCreateNestedOneWithoutAvailabilitiesInput = {
|
||||||
create?: Prisma.XOR<Prisma.CourtCreateWithoutAvailabilitiesInput, Prisma.CourtUncheckedCreateWithoutAvailabilitiesInput>
|
create?: Prisma.XOR<Prisma.CourtCreateWithoutAvailabilitiesInput, Prisma.CourtUncheckedCreateWithoutAvailabilitiesInput>
|
||||||
connectOrCreate?: Prisma.CourtCreateOrConnectWithoutAvailabilitiesInput
|
connectOrCreate?: Prisma.CourtCreateOrConnectWithoutAvailabilitiesInput
|
||||||
@@ -603,17 +675,35 @@ export type CourtUpdateOneRequiredWithoutBookingsNestedInput = {
|
|||||||
update?: Prisma.XOR<Prisma.XOR<Prisma.CourtUpdateToOneWithWhereWithoutBookingsInput, Prisma.CourtUpdateWithoutBookingsInput>, Prisma.CourtUncheckedUpdateWithoutBookingsInput>
|
update?: Prisma.XOR<Prisma.XOR<Prisma.CourtUpdateToOneWithWhereWithoutBookingsInput, Prisma.CourtUpdateWithoutBookingsInput>, Prisma.CourtUncheckedUpdateWithoutBookingsInput>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type CourtCreateNestedOneWithoutRecurringGroupsInput = {
|
||||||
|
create?: Prisma.XOR<Prisma.CourtCreateWithoutRecurringGroupsInput, Prisma.CourtUncheckedCreateWithoutRecurringGroupsInput>
|
||||||
|
connectOrCreate?: Prisma.CourtCreateOrConnectWithoutRecurringGroupsInput
|
||||||
|
connect?: Prisma.CourtWhereUniqueInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CourtUpdateOneRequiredWithoutRecurringGroupsNestedInput = {
|
||||||
|
create?: Prisma.XOR<Prisma.CourtCreateWithoutRecurringGroupsInput, Prisma.CourtUncheckedCreateWithoutRecurringGroupsInput>
|
||||||
|
connectOrCreate?: Prisma.CourtCreateOrConnectWithoutRecurringGroupsInput
|
||||||
|
upsert?: Prisma.CourtUpsertWithoutRecurringGroupsInput
|
||||||
|
connect?: Prisma.CourtWhereUniqueInput
|
||||||
|
update?: Prisma.XOR<Prisma.XOR<Prisma.CourtUpdateToOneWithWhereWithoutRecurringGroupsInput, Prisma.CourtUpdateWithoutRecurringGroupsInput>, Prisma.CourtUncheckedUpdateWithoutRecurringGroupsInput>
|
||||||
|
}
|
||||||
|
|
||||||
export type CourtCreateWithoutComplexInput = {
|
export type CourtCreateWithoutComplexInput = {
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
slotDurationMinutes: number
|
slotDurationMinutes: number
|
||||||
basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string
|
basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string
|
||||||
|
isUnderMaintenance?: boolean
|
||||||
|
maintenanceReason?: string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
sport: Prisma.SportCreateNestedOneWithoutCourtsInput
|
sport: Prisma.SportCreateNestedOneWithoutCourtsInput
|
||||||
availabilities?: Prisma.CourtAvailabilityCreateNestedManyWithoutCourtInput
|
availabilities?: Prisma.CourtAvailabilityCreateNestedManyWithoutCourtInput
|
||||||
priceRules?: Prisma.CourtPriceRuleCreateNestedManyWithoutCourtInput
|
priceRules?: Prisma.CourtPriceRuleCreateNestedManyWithoutCourtInput
|
||||||
bookings?: Prisma.CourtBookingCreateNestedManyWithoutCourtInput
|
bookings?: Prisma.CourtBookingCreateNestedManyWithoutCourtInput
|
||||||
|
maintenances?: Prisma.CourtMaintenanceCreateNestedManyWithoutCourtInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupCreateNestedManyWithoutCourtInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CourtUncheckedCreateWithoutComplexInput = {
|
export type CourtUncheckedCreateWithoutComplexInput = {
|
||||||
@@ -622,11 +712,15 @@ export type CourtUncheckedCreateWithoutComplexInput = {
|
|||||||
name: string
|
name: string
|
||||||
slotDurationMinutes: number
|
slotDurationMinutes: number
|
||||||
basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string
|
basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string
|
||||||
|
isUnderMaintenance?: boolean
|
||||||
|
maintenanceReason?: string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
availabilities?: Prisma.CourtAvailabilityUncheckedCreateNestedManyWithoutCourtInput
|
availabilities?: Prisma.CourtAvailabilityUncheckedCreateNestedManyWithoutCourtInput
|
||||||
priceRules?: Prisma.CourtPriceRuleUncheckedCreateNestedManyWithoutCourtInput
|
priceRules?: Prisma.CourtPriceRuleUncheckedCreateNestedManyWithoutCourtInput
|
||||||
bookings?: Prisma.CourtBookingUncheckedCreateNestedManyWithoutCourtInput
|
bookings?: Prisma.CourtBookingUncheckedCreateNestedManyWithoutCourtInput
|
||||||
|
maintenances?: Prisma.CourtMaintenanceUncheckedCreateNestedManyWithoutCourtInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupUncheckedCreateNestedManyWithoutCourtInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CourtCreateOrConnectWithoutComplexInput = {
|
export type CourtCreateOrConnectWithoutComplexInput = {
|
||||||
@@ -665,6 +759,8 @@ export type CourtScalarWhereInput = {
|
|||||||
name?: Prisma.StringFilter<"Court"> | string
|
name?: Prisma.StringFilter<"Court"> | string
|
||||||
slotDurationMinutes?: Prisma.IntFilter<"Court"> | number
|
slotDurationMinutes?: Prisma.IntFilter<"Court"> | number
|
||||||
basePrice?: Prisma.DecimalFilter<"Court"> | runtime.Decimal | runtime.DecimalJsLike | number | string
|
basePrice?: Prisma.DecimalFilter<"Court"> | runtime.Decimal | runtime.DecimalJsLike | number | string
|
||||||
|
isUnderMaintenance?: Prisma.BoolFilter<"Court"> | boolean
|
||||||
|
maintenanceReason?: Prisma.StringNullableFilter<"Court"> | string | null
|
||||||
createdAt?: Prisma.DateTimeFilter<"Court"> | Date | string
|
createdAt?: Prisma.DateTimeFilter<"Court"> | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFilter<"Court"> | Date | string
|
updatedAt?: Prisma.DateTimeFilter<"Court"> | Date | string
|
||||||
}
|
}
|
||||||
@@ -674,12 +770,16 @@ export type CourtCreateWithoutSportInput = {
|
|||||||
name: string
|
name: string
|
||||||
slotDurationMinutes: number
|
slotDurationMinutes: number
|
||||||
basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string
|
basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string
|
||||||
|
isUnderMaintenance?: boolean
|
||||||
|
maintenanceReason?: string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
complex: Prisma.ComplexCreateNestedOneWithoutCourtsInput
|
complex: Prisma.ComplexCreateNestedOneWithoutCourtsInput
|
||||||
availabilities?: Prisma.CourtAvailabilityCreateNestedManyWithoutCourtInput
|
availabilities?: Prisma.CourtAvailabilityCreateNestedManyWithoutCourtInput
|
||||||
priceRules?: Prisma.CourtPriceRuleCreateNestedManyWithoutCourtInput
|
priceRules?: Prisma.CourtPriceRuleCreateNestedManyWithoutCourtInput
|
||||||
bookings?: Prisma.CourtBookingCreateNestedManyWithoutCourtInput
|
bookings?: Prisma.CourtBookingCreateNestedManyWithoutCourtInput
|
||||||
|
maintenances?: Prisma.CourtMaintenanceCreateNestedManyWithoutCourtInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupCreateNestedManyWithoutCourtInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CourtUncheckedCreateWithoutSportInput = {
|
export type CourtUncheckedCreateWithoutSportInput = {
|
||||||
@@ -688,11 +788,15 @@ export type CourtUncheckedCreateWithoutSportInput = {
|
|||||||
name: string
|
name: string
|
||||||
slotDurationMinutes: number
|
slotDurationMinutes: number
|
||||||
basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string
|
basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string
|
||||||
|
isUnderMaintenance?: boolean
|
||||||
|
maintenanceReason?: string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
availabilities?: Prisma.CourtAvailabilityUncheckedCreateNestedManyWithoutCourtInput
|
availabilities?: Prisma.CourtAvailabilityUncheckedCreateNestedManyWithoutCourtInput
|
||||||
priceRules?: Prisma.CourtPriceRuleUncheckedCreateNestedManyWithoutCourtInput
|
priceRules?: Prisma.CourtPriceRuleUncheckedCreateNestedManyWithoutCourtInput
|
||||||
bookings?: Prisma.CourtBookingUncheckedCreateNestedManyWithoutCourtInput
|
bookings?: Prisma.CourtBookingUncheckedCreateNestedManyWithoutCourtInput
|
||||||
|
maintenances?: Prisma.CourtMaintenanceUncheckedCreateNestedManyWithoutCourtInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupUncheckedCreateNestedManyWithoutCourtInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CourtCreateOrConnectWithoutSportInput = {
|
export type CourtCreateOrConnectWithoutSportInput = {
|
||||||
@@ -721,17 +825,105 @@ export type CourtUpdateManyWithWhereWithoutSportInput = {
|
|||||||
data: Prisma.XOR<Prisma.CourtUpdateManyMutationInput, Prisma.CourtUncheckedUpdateManyWithoutSportInput>
|
data: Prisma.XOR<Prisma.CourtUpdateManyMutationInput, Prisma.CourtUncheckedUpdateManyWithoutSportInput>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type CourtCreateWithoutMaintenancesInput = {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
slotDurationMinutes: number
|
||||||
|
basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string
|
||||||
|
isUnderMaintenance?: boolean
|
||||||
|
maintenanceReason?: string | null
|
||||||
|
createdAt?: Date | string
|
||||||
|
updatedAt?: Date | string
|
||||||
|
complex: Prisma.ComplexCreateNestedOneWithoutCourtsInput
|
||||||
|
sport: Prisma.SportCreateNestedOneWithoutCourtsInput
|
||||||
|
availabilities?: Prisma.CourtAvailabilityCreateNestedManyWithoutCourtInput
|
||||||
|
priceRules?: Prisma.CourtPriceRuleCreateNestedManyWithoutCourtInput
|
||||||
|
bookings?: Prisma.CourtBookingCreateNestedManyWithoutCourtInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupCreateNestedManyWithoutCourtInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CourtUncheckedCreateWithoutMaintenancesInput = {
|
||||||
|
id: string
|
||||||
|
complexId: string
|
||||||
|
sportId: string
|
||||||
|
name: string
|
||||||
|
slotDurationMinutes: number
|
||||||
|
basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string
|
||||||
|
isUnderMaintenance?: boolean
|
||||||
|
maintenanceReason?: string | null
|
||||||
|
createdAt?: Date | string
|
||||||
|
updatedAt?: Date | string
|
||||||
|
availabilities?: Prisma.CourtAvailabilityUncheckedCreateNestedManyWithoutCourtInput
|
||||||
|
priceRules?: Prisma.CourtPriceRuleUncheckedCreateNestedManyWithoutCourtInput
|
||||||
|
bookings?: Prisma.CourtBookingUncheckedCreateNestedManyWithoutCourtInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupUncheckedCreateNestedManyWithoutCourtInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CourtCreateOrConnectWithoutMaintenancesInput = {
|
||||||
|
where: Prisma.CourtWhereUniqueInput
|
||||||
|
create: Prisma.XOR<Prisma.CourtCreateWithoutMaintenancesInput, Prisma.CourtUncheckedCreateWithoutMaintenancesInput>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CourtUpsertWithoutMaintenancesInput = {
|
||||||
|
update: Prisma.XOR<Prisma.CourtUpdateWithoutMaintenancesInput, Prisma.CourtUncheckedUpdateWithoutMaintenancesInput>
|
||||||
|
create: Prisma.XOR<Prisma.CourtCreateWithoutMaintenancesInput, Prisma.CourtUncheckedCreateWithoutMaintenancesInput>
|
||||||
|
where?: Prisma.CourtWhereInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CourtUpdateToOneWithWhereWithoutMaintenancesInput = {
|
||||||
|
where?: Prisma.CourtWhereInput
|
||||||
|
data: Prisma.XOR<Prisma.CourtUpdateWithoutMaintenancesInput, Prisma.CourtUncheckedUpdateWithoutMaintenancesInput>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CourtUpdateWithoutMaintenancesInput = {
|
||||||
|
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
|
basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string
|
||||||
|
isUnderMaintenance?: Prisma.BoolFieldUpdateOperationsInput | boolean
|
||||||
|
maintenanceReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
complex?: Prisma.ComplexUpdateOneRequiredWithoutCourtsNestedInput
|
||||||
|
sport?: Prisma.SportUpdateOneRequiredWithoutCourtsNestedInput
|
||||||
|
availabilities?: Prisma.CourtAvailabilityUpdateManyWithoutCourtNestedInput
|
||||||
|
priceRules?: Prisma.CourtPriceRuleUpdateManyWithoutCourtNestedInput
|
||||||
|
bookings?: Prisma.CourtBookingUpdateManyWithoutCourtNestedInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupUpdateManyWithoutCourtNestedInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CourtUncheckedUpdateWithoutMaintenancesInput = {
|
||||||
|
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
complexId?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
sportId?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
|
basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string
|
||||||
|
isUnderMaintenance?: Prisma.BoolFieldUpdateOperationsInput | boolean
|
||||||
|
maintenanceReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
availabilities?: Prisma.CourtAvailabilityUncheckedUpdateManyWithoutCourtNestedInput
|
||||||
|
priceRules?: Prisma.CourtPriceRuleUncheckedUpdateManyWithoutCourtNestedInput
|
||||||
|
bookings?: Prisma.CourtBookingUncheckedUpdateManyWithoutCourtNestedInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupUncheckedUpdateManyWithoutCourtNestedInput
|
||||||
|
}
|
||||||
|
|
||||||
export type CourtCreateWithoutAvailabilitiesInput = {
|
export type CourtCreateWithoutAvailabilitiesInput = {
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
slotDurationMinutes: number
|
slotDurationMinutes: number
|
||||||
basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string
|
basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string
|
||||||
|
isUnderMaintenance?: boolean
|
||||||
|
maintenanceReason?: string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
complex: Prisma.ComplexCreateNestedOneWithoutCourtsInput
|
complex: Prisma.ComplexCreateNestedOneWithoutCourtsInput
|
||||||
sport: Prisma.SportCreateNestedOneWithoutCourtsInput
|
sport: Prisma.SportCreateNestedOneWithoutCourtsInput
|
||||||
priceRules?: Prisma.CourtPriceRuleCreateNestedManyWithoutCourtInput
|
priceRules?: Prisma.CourtPriceRuleCreateNestedManyWithoutCourtInput
|
||||||
bookings?: Prisma.CourtBookingCreateNestedManyWithoutCourtInput
|
bookings?: Prisma.CourtBookingCreateNestedManyWithoutCourtInput
|
||||||
|
maintenances?: Prisma.CourtMaintenanceCreateNestedManyWithoutCourtInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupCreateNestedManyWithoutCourtInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CourtUncheckedCreateWithoutAvailabilitiesInput = {
|
export type CourtUncheckedCreateWithoutAvailabilitiesInput = {
|
||||||
@@ -741,10 +933,14 @@ export type CourtUncheckedCreateWithoutAvailabilitiesInput = {
|
|||||||
name: string
|
name: string
|
||||||
slotDurationMinutes: number
|
slotDurationMinutes: number
|
||||||
basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string
|
basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string
|
||||||
|
isUnderMaintenance?: boolean
|
||||||
|
maintenanceReason?: string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
priceRules?: Prisma.CourtPriceRuleUncheckedCreateNestedManyWithoutCourtInput
|
priceRules?: Prisma.CourtPriceRuleUncheckedCreateNestedManyWithoutCourtInput
|
||||||
bookings?: Prisma.CourtBookingUncheckedCreateNestedManyWithoutCourtInput
|
bookings?: Prisma.CourtBookingUncheckedCreateNestedManyWithoutCourtInput
|
||||||
|
maintenances?: Prisma.CourtMaintenanceUncheckedCreateNestedManyWithoutCourtInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupUncheckedCreateNestedManyWithoutCourtInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CourtCreateOrConnectWithoutAvailabilitiesInput = {
|
export type CourtCreateOrConnectWithoutAvailabilitiesInput = {
|
||||||
@@ -768,12 +964,16 @@ export type CourtUpdateWithoutAvailabilitiesInput = {
|
|||||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number
|
slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string
|
basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string
|
||||||
|
isUnderMaintenance?: Prisma.BoolFieldUpdateOperationsInput | boolean
|
||||||
|
maintenanceReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
complex?: Prisma.ComplexUpdateOneRequiredWithoutCourtsNestedInput
|
complex?: Prisma.ComplexUpdateOneRequiredWithoutCourtsNestedInput
|
||||||
sport?: Prisma.SportUpdateOneRequiredWithoutCourtsNestedInput
|
sport?: Prisma.SportUpdateOneRequiredWithoutCourtsNestedInput
|
||||||
priceRules?: Prisma.CourtPriceRuleUpdateManyWithoutCourtNestedInput
|
priceRules?: Prisma.CourtPriceRuleUpdateManyWithoutCourtNestedInput
|
||||||
bookings?: Prisma.CourtBookingUpdateManyWithoutCourtNestedInput
|
bookings?: Prisma.CourtBookingUpdateManyWithoutCourtNestedInput
|
||||||
|
maintenances?: Prisma.CourtMaintenanceUpdateManyWithoutCourtNestedInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupUpdateManyWithoutCourtNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CourtUncheckedUpdateWithoutAvailabilitiesInput = {
|
export type CourtUncheckedUpdateWithoutAvailabilitiesInput = {
|
||||||
@@ -783,10 +983,14 @@ export type CourtUncheckedUpdateWithoutAvailabilitiesInput = {
|
|||||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number
|
slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string
|
basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string
|
||||||
|
isUnderMaintenance?: Prisma.BoolFieldUpdateOperationsInput | boolean
|
||||||
|
maintenanceReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
priceRules?: Prisma.CourtPriceRuleUncheckedUpdateManyWithoutCourtNestedInput
|
priceRules?: Prisma.CourtPriceRuleUncheckedUpdateManyWithoutCourtNestedInput
|
||||||
bookings?: Prisma.CourtBookingUncheckedUpdateManyWithoutCourtNestedInput
|
bookings?: Prisma.CourtBookingUncheckedUpdateManyWithoutCourtNestedInput
|
||||||
|
maintenances?: Prisma.CourtMaintenanceUncheckedUpdateManyWithoutCourtNestedInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupUncheckedUpdateManyWithoutCourtNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CourtCreateWithoutPriceRulesInput = {
|
export type CourtCreateWithoutPriceRulesInput = {
|
||||||
@@ -794,12 +998,16 @@ export type CourtCreateWithoutPriceRulesInput = {
|
|||||||
name: string
|
name: string
|
||||||
slotDurationMinutes: number
|
slotDurationMinutes: number
|
||||||
basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string
|
basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string
|
||||||
|
isUnderMaintenance?: boolean
|
||||||
|
maintenanceReason?: string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
complex: Prisma.ComplexCreateNestedOneWithoutCourtsInput
|
complex: Prisma.ComplexCreateNestedOneWithoutCourtsInput
|
||||||
sport: Prisma.SportCreateNestedOneWithoutCourtsInput
|
sport: Prisma.SportCreateNestedOneWithoutCourtsInput
|
||||||
availabilities?: Prisma.CourtAvailabilityCreateNestedManyWithoutCourtInput
|
availabilities?: Prisma.CourtAvailabilityCreateNestedManyWithoutCourtInput
|
||||||
bookings?: Prisma.CourtBookingCreateNestedManyWithoutCourtInput
|
bookings?: Prisma.CourtBookingCreateNestedManyWithoutCourtInput
|
||||||
|
maintenances?: Prisma.CourtMaintenanceCreateNestedManyWithoutCourtInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupCreateNestedManyWithoutCourtInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CourtUncheckedCreateWithoutPriceRulesInput = {
|
export type CourtUncheckedCreateWithoutPriceRulesInput = {
|
||||||
@@ -809,10 +1017,14 @@ export type CourtUncheckedCreateWithoutPriceRulesInput = {
|
|||||||
name: string
|
name: string
|
||||||
slotDurationMinutes: number
|
slotDurationMinutes: number
|
||||||
basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string
|
basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string
|
||||||
|
isUnderMaintenance?: boolean
|
||||||
|
maintenanceReason?: string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
availabilities?: Prisma.CourtAvailabilityUncheckedCreateNestedManyWithoutCourtInput
|
availabilities?: Prisma.CourtAvailabilityUncheckedCreateNestedManyWithoutCourtInput
|
||||||
bookings?: Prisma.CourtBookingUncheckedCreateNestedManyWithoutCourtInput
|
bookings?: Prisma.CourtBookingUncheckedCreateNestedManyWithoutCourtInput
|
||||||
|
maintenances?: Prisma.CourtMaintenanceUncheckedCreateNestedManyWithoutCourtInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupUncheckedCreateNestedManyWithoutCourtInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CourtCreateOrConnectWithoutPriceRulesInput = {
|
export type CourtCreateOrConnectWithoutPriceRulesInput = {
|
||||||
@@ -836,12 +1048,16 @@ export type CourtUpdateWithoutPriceRulesInput = {
|
|||||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number
|
slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string
|
basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string
|
||||||
|
isUnderMaintenance?: Prisma.BoolFieldUpdateOperationsInput | boolean
|
||||||
|
maintenanceReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
complex?: Prisma.ComplexUpdateOneRequiredWithoutCourtsNestedInput
|
complex?: Prisma.ComplexUpdateOneRequiredWithoutCourtsNestedInput
|
||||||
sport?: Prisma.SportUpdateOneRequiredWithoutCourtsNestedInput
|
sport?: Prisma.SportUpdateOneRequiredWithoutCourtsNestedInput
|
||||||
availabilities?: Prisma.CourtAvailabilityUpdateManyWithoutCourtNestedInput
|
availabilities?: Prisma.CourtAvailabilityUpdateManyWithoutCourtNestedInput
|
||||||
bookings?: Prisma.CourtBookingUpdateManyWithoutCourtNestedInput
|
bookings?: Prisma.CourtBookingUpdateManyWithoutCourtNestedInput
|
||||||
|
maintenances?: Prisma.CourtMaintenanceUpdateManyWithoutCourtNestedInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupUpdateManyWithoutCourtNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CourtUncheckedUpdateWithoutPriceRulesInput = {
|
export type CourtUncheckedUpdateWithoutPriceRulesInput = {
|
||||||
@@ -851,10 +1067,14 @@ export type CourtUncheckedUpdateWithoutPriceRulesInput = {
|
|||||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number
|
slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string
|
basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string
|
||||||
|
isUnderMaintenance?: Prisma.BoolFieldUpdateOperationsInput | boolean
|
||||||
|
maintenanceReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
availabilities?: Prisma.CourtAvailabilityUncheckedUpdateManyWithoutCourtNestedInput
|
availabilities?: Prisma.CourtAvailabilityUncheckedUpdateManyWithoutCourtNestedInput
|
||||||
bookings?: Prisma.CourtBookingUncheckedUpdateManyWithoutCourtNestedInput
|
bookings?: Prisma.CourtBookingUncheckedUpdateManyWithoutCourtNestedInput
|
||||||
|
maintenances?: Prisma.CourtMaintenanceUncheckedUpdateManyWithoutCourtNestedInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupUncheckedUpdateManyWithoutCourtNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CourtCreateWithoutBookingsInput = {
|
export type CourtCreateWithoutBookingsInput = {
|
||||||
@@ -862,12 +1082,16 @@ export type CourtCreateWithoutBookingsInput = {
|
|||||||
name: string
|
name: string
|
||||||
slotDurationMinutes: number
|
slotDurationMinutes: number
|
||||||
basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string
|
basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string
|
||||||
|
isUnderMaintenance?: boolean
|
||||||
|
maintenanceReason?: string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
complex: Prisma.ComplexCreateNestedOneWithoutCourtsInput
|
complex: Prisma.ComplexCreateNestedOneWithoutCourtsInput
|
||||||
sport: Prisma.SportCreateNestedOneWithoutCourtsInput
|
sport: Prisma.SportCreateNestedOneWithoutCourtsInput
|
||||||
availabilities?: Prisma.CourtAvailabilityCreateNestedManyWithoutCourtInput
|
availabilities?: Prisma.CourtAvailabilityCreateNestedManyWithoutCourtInput
|
||||||
priceRules?: Prisma.CourtPriceRuleCreateNestedManyWithoutCourtInput
|
priceRules?: Prisma.CourtPriceRuleCreateNestedManyWithoutCourtInput
|
||||||
|
maintenances?: Prisma.CourtMaintenanceCreateNestedManyWithoutCourtInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupCreateNestedManyWithoutCourtInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CourtUncheckedCreateWithoutBookingsInput = {
|
export type CourtUncheckedCreateWithoutBookingsInput = {
|
||||||
@@ -877,10 +1101,14 @@ export type CourtUncheckedCreateWithoutBookingsInput = {
|
|||||||
name: string
|
name: string
|
||||||
slotDurationMinutes: number
|
slotDurationMinutes: number
|
||||||
basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string
|
basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string
|
||||||
|
isUnderMaintenance?: boolean
|
||||||
|
maintenanceReason?: string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
availabilities?: Prisma.CourtAvailabilityUncheckedCreateNestedManyWithoutCourtInput
|
availabilities?: Prisma.CourtAvailabilityUncheckedCreateNestedManyWithoutCourtInput
|
||||||
priceRules?: Prisma.CourtPriceRuleUncheckedCreateNestedManyWithoutCourtInput
|
priceRules?: Prisma.CourtPriceRuleUncheckedCreateNestedManyWithoutCourtInput
|
||||||
|
maintenances?: Prisma.CourtMaintenanceUncheckedCreateNestedManyWithoutCourtInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupUncheckedCreateNestedManyWithoutCourtInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CourtCreateOrConnectWithoutBookingsInput = {
|
export type CourtCreateOrConnectWithoutBookingsInput = {
|
||||||
@@ -904,12 +1132,16 @@ export type CourtUpdateWithoutBookingsInput = {
|
|||||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number
|
slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string
|
basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string
|
||||||
|
isUnderMaintenance?: Prisma.BoolFieldUpdateOperationsInput | boolean
|
||||||
|
maintenanceReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
complex?: Prisma.ComplexUpdateOneRequiredWithoutCourtsNestedInput
|
complex?: Prisma.ComplexUpdateOneRequiredWithoutCourtsNestedInput
|
||||||
sport?: Prisma.SportUpdateOneRequiredWithoutCourtsNestedInput
|
sport?: Prisma.SportUpdateOneRequiredWithoutCourtsNestedInput
|
||||||
availabilities?: Prisma.CourtAvailabilityUpdateManyWithoutCourtNestedInput
|
availabilities?: Prisma.CourtAvailabilityUpdateManyWithoutCourtNestedInput
|
||||||
priceRules?: Prisma.CourtPriceRuleUpdateManyWithoutCourtNestedInput
|
priceRules?: Prisma.CourtPriceRuleUpdateManyWithoutCourtNestedInput
|
||||||
|
maintenances?: Prisma.CourtMaintenanceUpdateManyWithoutCourtNestedInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupUpdateManyWithoutCourtNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CourtUncheckedUpdateWithoutBookingsInput = {
|
export type CourtUncheckedUpdateWithoutBookingsInput = {
|
||||||
@@ -919,10 +1151,98 @@ export type CourtUncheckedUpdateWithoutBookingsInput = {
|
|||||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number
|
slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string
|
basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string
|
||||||
|
isUnderMaintenance?: Prisma.BoolFieldUpdateOperationsInput | boolean
|
||||||
|
maintenanceReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
availabilities?: Prisma.CourtAvailabilityUncheckedUpdateManyWithoutCourtNestedInput
|
availabilities?: Prisma.CourtAvailabilityUncheckedUpdateManyWithoutCourtNestedInput
|
||||||
priceRules?: Prisma.CourtPriceRuleUncheckedUpdateManyWithoutCourtNestedInput
|
priceRules?: Prisma.CourtPriceRuleUncheckedUpdateManyWithoutCourtNestedInput
|
||||||
|
maintenances?: Prisma.CourtMaintenanceUncheckedUpdateManyWithoutCourtNestedInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupUncheckedUpdateManyWithoutCourtNestedInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CourtCreateWithoutRecurringGroupsInput = {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
slotDurationMinutes: number
|
||||||
|
basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string
|
||||||
|
isUnderMaintenance?: boolean
|
||||||
|
maintenanceReason?: string | null
|
||||||
|
createdAt?: Date | string
|
||||||
|
updatedAt?: Date | string
|
||||||
|
complex: Prisma.ComplexCreateNestedOneWithoutCourtsInput
|
||||||
|
sport: Prisma.SportCreateNestedOneWithoutCourtsInput
|
||||||
|
availabilities?: Prisma.CourtAvailabilityCreateNestedManyWithoutCourtInput
|
||||||
|
priceRules?: Prisma.CourtPriceRuleCreateNestedManyWithoutCourtInput
|
||||||
|
bookings?: Prisma.CourtBookingCreateNestedManyWithoutCourtInput
|
||||||
|
maintenances?: Prisma.CourtMaintenanceCreateNestedManyWithoutCourtInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CourtUncheckedCreateWithoutRecurringGroupsInput = {
|
||||||
|
id: string
|
||||||
|
complexId: string
|
||||||
|
sportId: string
|
||||||
|
name: string
|
||||||
|
slotDurationMinutes: number
|
||||||
|
basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string
|
||||||
|
isUnderMaintenance?: boolean
|
||||||
|
maintenanceReason?: string | null
|
||||||
|
createdAt?: Date | string
|
||||||
|
updatedAt?: Date | string
|
||||||
|
availabilities?: Prisma.CourtAvailabilityUncheckedCreateNestedManyWithoutCourtInput
|
||||||
|
priceRules?: Prisma.CourtPriceRuleUncheckedCreateNestedManyWithoutCourtInput
|
||||||
|
bookings?: Prisma.CourtBookingUncheckedCreateNestedManyWithoutCourtInput
|
||||||
|
maintenances?: Prisma.CourtMaintenanceUncheckedCreateNestedManyWithoutCourtInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CourtCreateOrConnectWithoutRecurringGroupsInput = {
|
||||||
|
where: Prisma.CourtWhereUniqueInput
|
||||||
|
create: Prisma.XOR<Prisma.CourtCreateWithoutRecurringGroupsInput, Prisma.CourtUncheckedCreateWithoutRecurringGroupsInput>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CourtUpsertWithoutRecurringGroupsInput = {
|
||||||
|
update: Prisma.XOR<Prisma.CourtUpdateWithoutRecurringGroupsInput, Prisma.CourtUncheckedUpdateWithoutRecurringGroupsInput>
|
||||||
|
create: Prisma.XOR<Prisma.CourtCreateWithoutRecurringGroupsInput, Prisma.CourtUncheckedCreateWithoutRecurringGroupsInput>
|
||||||
|
where?: Prisma.CourtWhereInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CourtUpdateToOneWithWhereWithoutRecurringGroupsInput = {
|
||||||
|
where?: Prisma.CourtWhereInput
|
||||||
|
data: Prisma.XOR<Prisma.CourtUpdateWithoutRecurringGroupsInput, Prisma.CourtUncheckedUpdateWithoutRecurringGroupsInput>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CourtUpdateWithoutRecurringGroupsInput = {
|
||||||
|
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
|
basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string
|
||||||
|
isUnderMaintenance?: Prisma.BoolFieldUpdateOperationsInput | boolean
|
||||||
|
maintenanceReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
complex?: Prisma.ComplexUpdateOneRequiredWithoutCourtsNestedInput
|
||||||
|
sport?: Prisma.SportUpdateOneRequiredWithoutCourtsNestedInput
|
||||||
|
availabilities?: Prisma.CourtAvailabilityUpdateManyWithoutCourtNestedInput
|
||||||
|
priceRules?: Prisma.CourtPriceRuleUpdateManyWithoutCourtNestedInput
|
||||||
|
bookings?: Prisma.CourtBookingUpdateManyWithoutCourtNestedInput
|
||||||
|
maintenances?: Prisma.CourtMaintenanceUpdateManyWithoutCourtNestedInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CourtUncheckedUpdateWithoutRecurringGroupsInput = {
|
||||||
|
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
complexId?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
sportId?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
|
basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string
|
||||||
|
isUnderMaintenance?: Prisma.BoolFieldUpdateOperationsInput | boolean
|
||||||
|
maintenanceReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
availabilities?: Prisma.CourtAvailabilityUncheckedUpdateManyWithoutCourtNestedInput
|
||||||
|
priceRules?: Prisma.CourtPriceRuleUncheckedUpdateManyWithoutCourtNestedInput
|
||||||
|
bookings?: Prisma.CourtBookingUncheckedUpdateManyWithoutCourtNestedInput
|
||||||
|
maintenances?: Prisma.CourtMaintenanceUncheckedUpdateManyWithoutCourtNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CourtCreateManyComplexInput = {
|
export type CourtCreateManyComplexInput = {
|
||||||
@@ -931,6 +1251,8 @@ export type CourtCreateManyComplexInput = {
|
|||||||
name: string
|
name: string
|
||||||
slotDurationMinutes: number
|
slotDurationMinutes: number
|
||||||
basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string
|
basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string
|
||||||
|
isUnderMaintenance?: boolean
|
||||||
|
maintenanceReason?: string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
}
|
}
|
||||||
@@ -940,12 +1262,16 @@ export type CourtUpdateWithoutComplexInput = {
|
|||||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number
|
slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string
|
basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string
|
||||||
|
isUnderMaintenance?: Prisma.BoolFieldUpdateOperationsInput | boolean
|
||||||
|
maintenanceReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
sport?: Prisma.SportUpdateOneRequiredWithoutCourtsNestedInput
|
sport?: Prisma.SportUpdateOneRequiredWithoutCourtsNestedInput
|
||||||
availabilities?: Prisma.CourtAvailabilityUpdateManyWithoutCourtNestedInput
|
availabilities?: Prisma.CourtAvailabilityUpdateManyWithoutCourtNestedInput
|
||||||
priceRules?: Prisma.CourtPriceRuleUpdateManyWithoutCourtNestedInput
|
priceRules?: Prisma.CourtPriceRuleUpdateManyWithoutCourtNestedInput
|
||||||
bookings?: Prisma.CourtBookingUpdateManyWithoutCourtNestedInput
|
bookings?: Prisma.CourtBookingUpdateManyWithoutCourtNestedInput
|
||||||
|
maintenances?: Prisma.CourtMaintenanceUpdateManyWithoutCourtNestedInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupUpdateManyWithoutCourtNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CourtUncheckedUpdateWithoutComplexInput = {
|
export type CourtUncheckedUpdateWithoutComplexInput = {
|
||||||
@@ -954,11 +1280,15 @@ export type CourtUncheckedUpdateWithoutComplexInput = {
|
|||||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number
|
slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string
|
basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string
|
||||||
|
isUnderMaintenance?: Prisma.BoolFieldUpdateOperationsInput | boolean
|
||||||
|
maintenanceReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
availabilities?: Prisma.CourtAvailabilityUncheckedUpdateManyWithoutCourtNestedInput
|
availabilities?: Prisma.CourtAvailabilityUncheckedUpdateManyWithoutCourtNestedInput
|
||||||
priceRules?: Prisma.CourtPriceRuleUncheckedUpdateManyWithoutCourtNestedInput
|
priceRules?: Prisma.CourtPriceRuleUncheckedUpdateManyWithoutCourtNestedInput
|
||||||
bookings?: Prisma.CourtBookingUncheckedUpdateManyWithoutCourtNestedInput
|
bookings?: Prisma.CourtBookingUncheckedUpdateManyWithoutCourtNestedInput
|
||||||
|
maintenances?: Prisma.CourtMaintenanceUncheckedUpdateManyWithoutCourtNestedInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupUncheckedUpdateManyWithoutCourtNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CourtUncheckedUpdateManyWithoutComplexInput = {
|
export type CourtUncheckedUpdateManyWithoutComplexInput = {
|
||||||
@@ -967,6 +1297,8 @@ export type CourtUncheckedUpdateManyWithoutComplexInput = {
|
|||||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number
|
slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string
|
basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string
|
||||||
|
isUnderMaintenance?: Prisma.BoolFieldUpdateOperationsInput | boolean
|
||||||
|
maintenanceReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
}
|
}
|
||||||
@@ -977,6 +1309,8 @@ export type CourtCreateManySportInput = {
|
|||||||
name: string
|
name: string
|
||||||
slotDurationMinutes: number
|
slotDurationMinutes: number
|
||||||
basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string
|
basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string
|
||||||
|
isUnderMaintenance?: boolean
|
||||||
|
maintenanceReason?: string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
}
|
}
|
||||||
@@ -986,12 +1320,16 @@ export type CourtUpdateWithoutSportInput = {
|
|||||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number
|
slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string
|
basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string
|
||||||
|
isUnderMaintenance?: Prisma.BoolFieldUpdateOperationsInput | boolean
|
||||||
|
maintenanceReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
complex?: Prisma.ComplexUpdateOneRequiredWithoutCourtsNestedInput
|
complex?: Prisma.ComplexUpdateOneRequiredWithoutCourtsNestedInput
|
||||||
availabilities?: Prisma.CourtAvailabilityUpdateManyWithoutCourtNestedInput
|
availabilities?: Prisma.CourtAvailabilityUpdateManyWithoutCourtNestedInput
|
||||||
priceRules?: Prisma.CourtPriceRuleUpdateManyWithoutCourtNestedInput
|
priceRules?: Prisma.CourtPriceRuleUpdateManyWithoutCourtNestedInput
|
||||||
bookings?: Prisma.CourtBookingUpdateManyWithoutCourtNestedInput
|
bookings?: Prisma.CourtBookingUpdateManyWithoutCourtNestedInput
|
||||||
|
maintenances?: Prisma.CourtMaintenanceUpdateManyWithoutCourtNestedInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupUpdateManyWithoutCourtNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CourtUncheckedUpdateWithoutSportInput = {
|
export type CourtUncheckedUpdateWithoutSportInput = {
|
||||||
@@ -1000,11 +1338,15 @@ export type CourtUncheckedUpdateWithoutSportInput = {
|
|||||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number
|
slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string
|
basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string
|
||||||
|
isUnderMaintenance?: Prisma.BoolFieldUpdateOperationsInput | boolean
|
||||||
|
maintenanceReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
availabilities?: Prisma.CourtAvailabilityUncheckedUpdateManyWithoutCourtNestedInput
|
availabilities?: Prisma.CourtAvailabilityUncheckedUpdateManyWithoutCourtNestedInput
|
||||||
priceRules?: Prisma.CourtPriceRuleUncheckedUpdateManyWithoutCourtNestedInput
|
priceRules?: Prisma.CourtPriceRuleUncheckedUpdateManyWithoutCourtNestedInput
|
||||||
bookings?: Prisma.CourtBookingUncheckedUpdateManyWithoutCourtNestedInput
|
bookings?: Prisma.CourtBookingUncheckedUpdateManyWithoutCourtNestedInput
|
||||||
|
maintenances?: Prisma.CourtMaintenanceUncheckedUpdateManyWithoutCourtNestedInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupUncheckedUpdateManyWithoutCourtNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CourtUncheckedUpdateManyWithoutSportInput = {
|
export type CourtUncheckedUpdateManyWithoutSportInput = {
|
||||||
@@ -1013,6 +1355,8 @@ export type CourtUncheckedUpdateManyWithoutSportInput = {
|
|||||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number
|
slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string
|
basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string
|
||||||
|
isUnderMaintenance?: Prisma.BoolFieldUpdateOperationsInput | boolean
|
||||||
|
maintenanceReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
}
|
}
|
||||||
@@ -1026,12 +1370,16 @@ export type CourtCountOutputType = {
|
|||||||
availabilities: number
|
availabilities: number
|
||||||
priceRules: number
|
priceRules: number
|
||||||
bookings: number
|
bookings: number
|
||||||
|
maintenances: number
|
||||||
|
recurringGroups: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CourtCountOutputTypeSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
export type CourtCountOutputTypeSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
availabilities?: boolean | CourtCountOutputTypeCountAvailabilitiesArgs
|
availabilities?: boolean | CourtCountOutputTypeCountAvailabilitiesArgs
|
||||||
priceRules?: boolean | CourtCountOutputTypeCountPriceRulesArgs
|
priceRules?: boolean | CourtCountOutputTypeCountPriceRulesArgs
|
||||||
bookings?: boolean | CourtCountOutputTypeCountBookingsArgs
|
bookings?: boolean | CourtCountOutputTypeCountBookingsArgs
|
||||||
|
maintenances?: boolean | CourtCountOutputTypeCountMaintenancesArgs
|
||||||
|
recurringGroups?: boolean | CourtCountOutputTypeCountRecurringGroupsArgs
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1065,6 +1413,20 @@ export type CourtCountOutputTypeCountBookingsArgs<ExtArgs extends runtime.Types.
|
|||||||
where?: Prisma.CourtBookingWhereInput
|
where?: Prisma.CourtBookingWhereInput
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CourtCountOutputType without action
|
||||||
|
*/
|
||||||
|
export type CourtCountOutputTypeCountMaintenancesArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
|
where?: Prisma.CourtMaintenanceWhereInput
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CourtCountOutputType without action
|
||||||
|
*/
|
||||||
|
export type CourtCountOutputTypeCountRecurringGroupsArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
|
where?: Prisma.RecurringBookingGroupWhereInput
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export type CourtSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
|
export type CourtSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
|
||||||
id?: boolean
|
id?: boolean
|
||||||
@@ -1073,6 +1435,8 @@ export type CourtSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs =
|
|||||||
name?: boolean
|
name?: boolean
|
||||||
slotDurationMinutes?: boolean
|
slotDurationMinutes?: boolean
|
||||||
basePrice?: boolean
|
basePrice?: boolean
|
||||||
|
isUnderMaintenance?: boolean
|
||||||
|
maintenanceReason?: boolean
|
||||||
createdAt?: boolean
|
createdAt?: boolean
|
||||||
updatedAt?: boolean
|
updatedAt?: boolean
|
||||||
complex?: boolean | Prisma.ComplexDefaultArgs<ExtArgs>
|
complex?: boolean | Prisma.ComplexDefaultArgs<ExtArgs>
|
||||||
@@ -1080,6 +1444,8 @@ export type CourtSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs =
|
|||||||
availabilities?: boolean | Prisma.Court$availabilitiesArgs<ExtArgs>
|
availabilities?: boolean | Prisma.Court$availabilitiesArgs<ExtArgs>
|
||||||
priceRules?: boolean | Prisma.Court$priceRulesArgs<ExtArgs>
|
priceRules?: boolean | Prisma.Court$priceRulesArgs<ExtArgs>
|
||||||
bookings?: boolean | Prisma.Court$bookingsArgs<ExtArgs>
|
bookings?: boolean | Prisma.Court$bookingsArgs<ExtArgs>
|
||||||
|
maintenances?: boolean | Prisma.Court$maintenancesArgs<ExtArgs>
|
||||||
|
recurringGroups?: boolean | Prisma.Court$recurringGroupsArgs<ExtArgs>
|
||||||
_count?: boolean | Prisma.CourtCountOutputTypeDefaultArgs<ExtArgs>
|
_count?: boolean | Prisma.CourtCountOutputTypeDefaultArgs<ExtArgs>
|
||||||
}, ExtArgs["result"]["court"]>
|
}, ExtArgs["result"]["court"]>
|
||||||
|
|
||||||
@@ -1090,6 +1456,8 @@ export type CourtSelectCreateManyAndReturn<ExtArgs extends runtime.Types.Extensi
|
|||||||
name?: boolean
|
name?: boolean
|
||||||
slotDurationMinutes?: boolean
|
slotDurationMinutes?: boolean
|
||||||
basePrice?: boolean
|
basePrice?: boolean
|
||||||
|
isUnderMaintenance?: boolean
|
||||||
|
maintenanceReason?: boolean
|
||||||
createdAt?: boolean
|
createdAt?: boolean
|
||||||
updatedAt?: boolean
|
updatedAt?: boolean
|
||||||
complex?: boolean | Prisma.ComplexDefaultArgs<ExtArgs>
|
complex?: boolean | Prisma.ComplexDefaultArgs<ExtArgs>
|
||||||
@@ -1103,6 +1471,8 @@ export type CourtSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensi
|
|||||||
name?: boolean
|
name?: boolean
|
||||||
slotDurationMinutes?: boolean
|
slotDurationMinutes?: boolean
|
||||||
basePrice?: boolean
|
basePrice?: boolean
|
||||||
|
isUnderMaintenance?: boolean
|
||||||
|
maintenanceReason?: boolean
|
||||||
createdAt?: boolean
|
createdAt?: boolean
|
||||||
updatedAt?: boolean
|
updatedAt?: boolean
|
||||||
complex?: boolean | Prisma.ComplexDefaultArgs<ExtArgs>
|
complex?: boolean | Prisma.ComplexDefaultArgs<ExtArgs>
|
||||||
@@ -1116,17 +1486,21 @@ export type CourtSelectScalar = {
|
|||||||
name?: boolean
|
name?: boolean
|
||||||
slotDurationMinutes?: boolean
|
slotDurationMinutes?: boolean
|
||||||
basePrice?: boolean
|
basePrice?: boolean
|
||||||
|
isUnderMaintenance?: boolean
|
||||||
|
maintenanceReason?: boolean
|
||||||
createdAt?: boolean
|
createdAt?: boolean
|
||||||
updatedAt?: boolean
|
updatedAt?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CourtOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "complexId" | "sportId" | "name" | "slotDurationMinutes" | "basePrice" | "createdAt" | "updatedAt", ExtArgs["result"]["court"]>
|
export type CourtOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "complexId" | "sportId" | "name" | "slotDurationMinutes" | "basePrice" | "isUnderMaintenance" | "maintenanceReason" | "createdAt" | "updatedAt", ExtArgs["result"]["court"]>
|
||||||
export type CourtInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
export type CourtInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
complex?: boolean | Prisma.ComplexDefaultArgs<ExtArgs>
|
complex?: boolean | Prisma.ComplexDefaultArgs<ExtArgs>
|
||||||
sport?: boolean | Prisma.SportDefaultArgs<ExtArgs>
|
sport?: boolean | Prisma.SportDefaultArgs<ExtArgs>
|
||||||
availabilities?: boolean | Prisma.Court$availabilitiesArgs<ExtArgs>
|
availabilities?: boolean | Prisma.Court$availabilitiesArgs<ExtArgs>
|
||||||
priceRules?: boolean | Prisma.Court$priceRulesArgs<ExtArgs>
|
priceRules?: boolean | Prisma.Court$priceRulesArgs<ExtArgs>
|
||||||
bookings?: boolean | Prisma.Court$bookingsArgs<ExtArgs>
|
bookings?: boolean | Prisma.Court$bookingsArgs<ExtArgs>
|
||||||
|
maintenances?: boolean | Prisma.Court$maintenancesArgs<ExtArgs>
|
||||||
|
recurringGroups?: boolean | Prisma.Court$recurringGroupsArgs<ExtArgs>
|
||||||
_count?: boolean | Prisma.CourtCountOutputTypeDefaultArgs<ExtArgs>
|
_count?: boolean | Prisma.CourtCountOutputTypeDefaultArgs<ExtArgs>
|
||||||
}
|
}
|
||||||
export type CourtIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
export type CourtIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
@@ -1146,6 +1520,8 @@ export type $CourtPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs
|
|||||||
availabilities: Prisma.$CourtAvailabilityPayload<ExtArgs>[]
|
availabilities: Prisma.$CourtAvailabilityPayload<ExtArgs>[]
|
||||||
priceRules: Prisma.$CourtPriceRulePayload<ExtArgs>[]
|
priceRules: Prisma.$CourtPriceRulePayload<ExtArgs>[]
|
||||||
bookings: Prisma.$CourtBookingPayload<ExtArgs>[]
|
bookings: Prisma.$CourtBookingPayload<ExtArgs>[]
|
||||||
|
maintenances: Prisma.$CourtMaintenancePayload<ExtArgs>[]
|
||||||
|
recurringGroups: Prisma.$RecurringBookingGroupPayload<ExtArgs>[]
|
||||||
}
|
}
|
||||||
scalars: runtime.Types.Extensions.GetPayloadResult<{
|
scalars: runtime.Types.Extensions.GetPayloadResult<{
|
||||||
id: string
|
id: string
|
||||||
@@ -1154,6 +1530,8 @@ export type $CourtPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs
|
|||||||
name: string
|
name: string
|
||||||
slotDurationMinutes: number
|
slotDurationMinutes: number
|
||||||
basePrice: runtime.Decimal
|
basePrice: runtime.Decimal
|
||||||
|
isUnderMaintenance: boolean
|
||||||
|
maintenanceReason: string | null
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
updatedAt: Date
|
updatedAt: Date
|
||||||
}, ExtArgs["result"]["court"]>
|
}, ExtArgs["result"]["court"]>
|
||||||
@@ -1555,6 +1933,8 @@ export interface Prisma__CourtClient<T, Null = never, ExtArgs extends runtime.Ty
|
|||||||
availabilities<T extends Prisma.Court$availabilitiesArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Court$availabilitiesArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$CourtAvailabilityPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
availabilities<T extends Prisma.Court$availabilitiesArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Court$availabilitiesArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$CourtAvailabilityPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
||||||
priceRules<T extends Prisma.Court$priceRulesArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Court$priceRulesArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$CourtPriceRulePayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
priceRules<T extends Prisma.Court$priceRulesArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Court$priceRulesArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$CourtPriceRulePayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
||||||
bookings<T extends Prisma.Court$bookingsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Court$bookingsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$CourtBookingPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
bookings<T extends Prisma.Court$bookingsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Court$bookingsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$CourtBookingPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
||||||
|
maintenances<T extends Prisma.Court$maintenancesArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Court$maintenancesArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$CourtMaintenancePayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
||||||
|
recurringGroups<T extends Prisma.Court$recurringGroupsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Court$recurringGroupsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$RecurringBookingGroupPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
||||||
/**
|
/**
|
||||||
* Attaches callbacks for the resolution and/or rejection of the Promise.
|
* Attaches callbacks for the resolution and/or rejection of the Promise.
|
||||||
* @param onfulfilled The callback to execute when the Promise is resolved.
|
* @param onfulfilled The callback to execute when the Promise is resolved.
|
||||||
@@ -1590,6 +1970,8 @@ export interface CourtFieldRefs {
|
|||||||
readonly name: Prisma.FieldRef<"Court", 'String'>
|
readonly name: Prisma.FieldRef<"Court", 'String'>
|
||||||
readonly slotDurationMinutes: Prisma.FieldRef<"Court", 'Int'>
|
readonly slotDurationMinutes: Prisma.FieldRef<"Court", 'Int'>
|
||||||
readonly basePrice: Prisma.FieldRef<"Court", 'Decimal'>
|
readonly basePrice: Prisma.FieldRef<"Court", 'Decimal'>
|
||||||
|
readonly isUnderMaintenance: Prisma.FieldRef<"Court", 'Boolean'>
|
||||||
|
readonly maintenanceReason: Prisma.FieldRef<"Court", 'String'>
|
||||||
readonly createdAt: Prisma.FieldRef<"Court", 'DateTime'>
|
readonly createdAt: Prisma.FieldRef<"Court", 'DateTime'>
|
||||||
readonly updatedAt: Prisma.FieldRef<"Court", 'DateTime'>
|
readonly updatedAt: Prisma.FieldRef<"Court", 'DateTime'>
|
||||||
}
|
}
|
||||||
@@ -2064,6 +2446,54 @@ export type Court$bookingsArgs<ExtArgs extends runtime.Types.Extensions.Internal
|
|||||||
distinct?: Prisma.CourtBookingScalarFieldEnum | Prisma.CourtBookingScalarFieldEnum[]
|
distinct?: Prisma.CourtBookingScalarFieldEnum | Prisma.CourtBookingScalarFieldEnum[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Court.maintenances
|
||||||
|
*/
|
||||||
|
export type Court$maintenancesArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
|
/**
|
||||||
|
* Select specific fields to fetch from the CourtMaintenance
|
||||||
|
*/
|
||||||
|
select?: Prisma.CourtMaintenanceSelect<ExtArgs> | null
|
||||||
|
/**
|
||||||
|
* Omit specific fields from the CourtMaintenance
|
||||||
|
*/
|
||||||
|
omit?: Prisma.CourtMaintenanceOmit<ExtArgs> | null
|
||||||
|
/**
|
||||||
|
* Choose, which related nodes to fetch as well
|
||||||
|
*/
|
||||||
|
include?: Prisma.CourtMaintenanceInclude<ExtArgs> | null
|
||||||
|
where?: Prisma.CourtMaintenanceWhereInput
|
||||||
|
orderBy?: Prisma.CourtMaintenanceOrderByWithRelationInput | Prisma.CourtMaintenanceOrderByWithRelationInput[]
|
||||||
|
cursor?: Prisma.CourtMaintenanceWhereUniqueInput
|
||||||
|
take?: number
|
||||||
|
skip?: number
|
||||||
|
distinct?: Prisma.CourtMaintenanceScalarFieldEnum | Prisma.CourtMaintenanceScalarFieldEnum[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Court.recurringGroups
|
||||||
|
*/
|
||||||
|
export type Court$recurringGroupsArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
|
/**
|
||||||
|
* Select specific fields to fetch from the RecurringBookingGroup
|
||||||
|
*/
|
||||||
|
select?: Prisma.RecurringBookingGroupSelect<ExtArgs> | null
|
||||||
|
/**
|
||||||
|
* Omit specific fields from the RecurringBookingGroup
|
||||||
|
*/
|
||||||
|
omit?: Prisma.RecurringBookingGroupOmit<ExtArgs> | null
|
||||||
|
/**
|
||||||
|
* Choose, which related nodes to fetch as well
|
||||||
|
*/
|
||||||
|
include?: Prisma.RecurringBookingGroupInclude<ExtArgs> | null
|
||||||
|
where?: Prisma.RecurringBookingGroupWhereInput
|
||||||
|
orderBy?: Prisma.RecurringBookingGroupOrderByWithRelationInput | Prisma.RecurringBookingGroupOrderByWithRelationInput[]
|
||||||
|
cursor?: Prisma.RecurringBookingGroupWhereUniqueInput
|
||||||
|
take?: number
|
||||||
|
skip?: number
|
||||||
|
distinct?: Prisma.RecurringBookingGroupScalarFieldEnum | Prisma.RecurringBookingGroupScalarFieldEnum[]
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Court without action
|
* Court without action
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -33,7 +33,9 @@ export type CourtBookingMinAggregateOutputType = {
|
|||||||
endTime: string | null
|
endTime: string | null
|
||||||
customerName: string | null
|
customerName: string | null
|
||||||
customerPhone: string | null
|
customerPhone: string | null
|
||||||
|
customerEmail: string | null
|
||||||
status: $Enums.CourtBookingStatus | null
|
status: $Enums.CourtBookingStatus | null
|
||||||
|
recurringGroupId: string | null
|
||||||
createdAt: Date | null
|
createdAt: Date | null
|
||||||
updatedAt: Date | null
|
updatedAt: Date | null
|
||||||
}
|
}
|
||||||
@@ -47,7 +49,9 @@ export type CourtBookingMaxAggregateOutputType = {
|
|||||||
endTime: string | null
|
endTime: string | null
|
||||||
customerName: string | null
|
customerName: string | null
|
||||||
customerPhone: string | null
|
customerPhone: string | null
|
||||||
|
customerEmail: string | null
|
||||||
status: $Enums.CourtBookingStatus | null
|
status: $Enums.CourtBookingStatus | null
|
||||||
|
recurringGroupId: string | null
|
||||||
createdAt: Date | null
|
createdAt: Date | null
|
||||||
updatedAt: Date | null
|
updatedAt: Date | null
|
||||||
}
|
}
|
||||||
@@ -61,7 +65,9 @@ export type CourtBookingCountAggregateOutputType = {
|
|||||||
endTime: number
|
endTime: number
|
||||||
customerName: number
|
customerName: number
|
||||||
customerPhone: number
|
customerPhone: number
|
||||||
|
customerEmail: number
|
||||||
status: number
|
status: number
|
||||||
|
recurringGroupId: number
|
||||||
createdAt: number
|
createdAt: number
|
||||||
updatedAt: number
|
updatedAt: number
|
||||||
_all: number
|
_all: number
|
||||||
@@ -77,7 +83,9 @@ export type CourtBookingMinAggregateInputType = {
|
|||||||
endTime?: true
|
endTime?: true
|
||||||
customerName?: true
|
customerName?: true
|
||||||
customerPhone?: true
|
customerPhone?: true
|
||||||
|
customerEmail?: true
|
||||||
status?: true
|
status?: true
|
||||||
|
recurringGroupId?: true
|
||||||
createdAt?: true
|
createdAt?: true
|
||||||
updatedAt?: true
|
updatedAt?: true
|
||||||
}
|
}
|
||||||
@@ -91,7 +99,9 @@ export type CourtBookingMaxAggregateInputType = {
|
|||||||
endTime?: true
|
endTime?: true
|
||||||
customerName?: true
|
customerName?: true
|
||||||
customerPhone?: true
|
customerPhone?: true
|
||||||
|
customerEmail?: true
|
||||||
status?: true
|
status?: true
|
||||||
|
recurringGroupId?: true
|
||||||
createdAt?: true
|
createdAt?: true
|
||||||
updatedAt?: true
|
updatedAt?: true
|
||||||
}
|
}
|
||||||
@@ -105,7 +115,9 @@ export type CourtBookingCountAggregateInputType = {
|
|||||||
endTime?: true
|
endTime?: true
|
||||||
customerName?: true
|
customerName?: true
|
||||||
customerPhone?: true
|
customerPhone?: true
|
||||||
|
customerEmail?: true
|
||||||
status?: true
|
status?: true
|
||||||
|
recurringGroupId?: true
|
||||||
createdAt?: true
|
createdAt?: true
|
||||||
updatedAt?: true
|
updatedAt?: true
|
||||||
_all?: true
|
_all?: true
|
||||||
@@ -192,7 +204,9 @@ export type CourtBookingGroupByOutputType = {
|
|||||||
endTime: string
|
endTime: string
|
||||||
customerName: string
|
customerName: string
|
||||||
customerPhone: string
|
customerPhone: string
|
||||||
|
customerEmail: string
|
||||||
status: $Enums.CourtBookingStatus
|
status: $Enums.CourtBookingStatus
|
||||||
|
recurringGroupId: string | null
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
updatedAt: Date
|
updatedAt: Date
|
||||||
_count: CourtBookingCountAggregateOutputType | null
|
_count: CourtBookingCountAggregateOutputType | null
|
||||||
@@ -227,10 +241,13 @@ export type CourtBookingWhereInput = {
|
|||||||
endTime?: Prisma.StringFilter<"CourtBooking"> | string
|
endTime?: Prisma.StringFilter<"CourtBooking"> | string
|
||||||
customerName?: Prisma.StringFilter<"CourtBooking"> | string
|
customerName?: Prisma.StringFilter<"CourtBooking"> | string
|
||||||
customerPhone?: Prisma.StringFilter<"CourtBooking"> | string
|
customerPhone?: Prisma.StringFilter<"CourtBooking"> | string
|
||||||
|
customerEmail?: Prisma.StringFilter<"CourtBooking"> | string
|
||||||
status?: Prisma.EnumCourtBookingStatusFilter<"CourtBooking"> | $Enums.CourtBookingStatus
|
status?: Prisma.EnumCourtBookingStatusFilter<"CourtBooking"> | $Enums.CourtBookingStatus
|
||||||
|
recurringGroupId?: Prisma.UuidNullableFilter<"CourtBooking"> | string | null
|
||||||
createdAt?: Prisma.DateTimeFilter<"CourtBooking"> | Date | string
|
createdAt?: Prisma.DateTimeFilter<"CourtBooking"> | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFilter<"CourtBooking"> | Date | string
|
updatedAt?: Prisma.DateTimeFilter<"CourtBooking"> | Date | string
|
||||||
court?: Prisma.XOR<Prisma.CourtScalarRelationFilter, Prisma.CourtWhereInput>
|
court?: Prisma.XOR<Prisma.CourtScalarRelationFilter, Prisma.CourtWhereInput>
|
||||||
|
recurringGroup?: Prisma.XOR<Prisma.RecurringBookingGroupNullableScalarRelationFilter, Prisma.RecurringBookingGroupWhereInput> | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CourtBookingOrderByWithRelationInput = {
|
export type CourtBookingOrderByWithRelationInput = {
|
||||||
@@ -242,10 +259,13 @@ export type CourtBookingOrderByWithRelationInput = {
|
|||||||
endTime?: Prisma.SortOrder
|
endTime?: Prisma.SortOrder
|
||||||
customerName?: Prisma.SortOrder
|
customerName?: Prisma.SortOrder
|
||||||
customerPhone?: Prisma.SortOrder
|
customerPhone?: Prisma.SortOrder
|
||||||
|
customerEmail?: Prisma.SortOrder
|
||||||
status?: Prisma.SortOrder
|
status?: Prisma.SortOrder
|
||||||
|
recurringGroupId?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
createdAt?: Prisma.SortOrder
|
createdAt?: Prisma.SortOrder
|
||||||
updatedAt?: Prisma.SortOrder
|
updatedAt?: Prisma.SortOrder
|
||||||
court?: Prisma.CourtOrderByWithRelationInput
|
court?: Prisma.CourtOrderByWithRelationInput
|
||||||
|
recurringGroup?: Prisma.RecurringBookingGroupOrderByWithRelationInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CourtBookingWhereUniqueInput = Prisma.AtLeast<{
|
export type CourtBookingWhereUniqueInput = Prisma.AtLeast<{
|
||||||
@@ -261,10 +281,13 @@ export type CourtBookingWhereUniqueInput = Prisma.AtLeast<{
|
|||||||
endTime?: Prisma.StringFilter<"CourtBooking"> | string
|
endTime?: Prisma.StringFilter<"CourtBooking"> | string
|
||||||
customerName?: Prisma.StringFilter<"CourtBooking"> | string
|
customerName?: Prisma.StringFilter<"CourtBooking"> | string
|
||||||
customerPhone?: Prisma.StringFilter<"CourtBooking"> | string
|
customerPhone?: Prisma.StringFilter<"CourtBooking"> | string
|
||||||
|
customerEmail?: Prisma.StringFilter<"CourtBooking"> | string
|
||||||
status?: Prisma.EnumCourtBookingStatusFilter<"CourtBooking"> | $Enums.CourtBookingStatus
|
status?: Prisma.EnumCourtBookingStatusFilter<"CourtBooking"> | $Enums.CourtBookingStatus
|
||||||
|
recurringGroupId?: Prisma.UuidNullableFilter<"CourtBooking"> | string | null
|
||||||
createdAt?: Prisma.DateTimeFilter<"CourtBooking"> | Date | string
|
createdAt?: Prisma.DateTimeFilter<"CourtBooking"> | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFilter<"CourtBooking"> | Date | string
|
updatedAt?: Prisma.DateTimeFilter<"CourtBooking"> | Date | string
|
||||||
court?: Prisma.XOR<Prisma.CourtScalarRelationFilter, Prisma.CourtWhereInput>
|
court?: Prisma.XOR<Prisma.CourtScalarRelationFilter, Prisma.CourtWhereInput>
|
||||||
|
recurringGroup?: Prisma.XOR<Prisma.RecurringBookingGroupNullableScalarRelationFilter, Prisma.RecurringBookingGroupWhereInput> | null
|
||||||
}, "id" | "bookingCode" | "courtId_bookingDate_startTime">
|
}, "id" | "bookingCode" | "courtId_bookingDate_startTime">
|
||||||
|
|
||||||
export type CourtBookingOrderByWithAggregationInput = {
|
export type CourtBookingOrderByWithAggregationInput = {
|
||||||
@@ -276,7 +299,9 @@ export type CourtBookingOrderByWithAggregationInput = {
|
|||||||
endTime?: Prisma.SortOrder
|
endTime?: Prisma.SortOrder
|
||||||
customerName?: Prisma.SortOrder
|
customerName?: Prisma.SortOrder
|
||||||
customerPhone?: Prisma.SortOrder
|
customerPhone?: Prisma.SortOrder
|
||||||
|
customerEmail?: Prisma.SortOrder
|
||||||
status?: Prisma.SortOrder
|
status?: Prisma.SortOrder
|
||||||
|
recurringGroupId?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
createdAt?: Prisma.SortOrder
|
createdAt?: Prisma.SortOrder
|
||||||
updatedAt?: Prisma.SortOrder
|
updatedAt?: Prisma.SortOrder
|
||||||
_count?: Prisma.CourtBookingCountOrderByAggregateInput
|
_count?: Prisma.CourtBookingCountOrderByAggregateInput
|
||||||
@@ -296,7 +321,9 @@ export type CourtBookingScalarWhereWithAggregatesInput = {
|
|||||||
endTime?: Prisma.StringWithAggregatesFilter<"CourtBooking"> | string
|
endTime?: Prisma.StringWithAggregatesFilter<"CourtBooking"> | string
|
||||||
customerName?: Prisma.StringWithAggregatesFilter<"CourtBooking"> | string
|
customerName?: Prisma.StringWithAggregatesFilter<"CourtBooking"> | string
|
||||||
customerPhone?: Prisma.StringWithAggregatesFilter<"CourtBooking"> | string
|
customerPhone?: Prisma.StringWithAggregatesFilter<"CourtBooking"> | string
|
||||||
|
customerEmail?: Prisma.StringWithAggregatesFilter<"CourtBooking"> | string
|
||||||
status?: Prisma.EnumCourtBookingStatusWithAggregatesFilter<"CourtBooking"> | $Enums.CourtBookingStatus
|
status?: Prisma.EnumCourtBookingStatusWithAggregatesFilter<"CourtBooking"> | $Enums.CourtBookingStatus
|
||||||
|
recurringGroupId?: Prisma.UuidNullableWithAggregatesFilter<"CourtBooking"> | string | null
|
||||||
createdAt?: Prisma.DateTimeWithAggregatesFilter<"CourtBooking"> | Date | string
|
createdAt?: Prisma.DateTimeWithAggregatesFilter<"CourtBooking"> | Date | string
|
||||||
updatedAt?: Prisma.DateTimeWithAggregatesFilter<"CourtBooking"> | Date | string
|
updatedAt?: Prisma.DateTimeWithAggregatesFilter<"CourtBooking"> | Date | string
|
||||||
}
|
}
|
||||||
@@ -309,10 +336,12 @@ export type CourtBookingCreateInput = {
|
|||||||
endTime: string
|
endTime: string
|
||||||
customerName: string
|
customerName: string
|
||||||
customerPhone: string
|
customerPhone: string
|
||||||
|
customerEmail: string
|
||||||
status?: $Enums.CourtBookingStatus
|
status?: $Enums.CourtBookingStatus
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
court: Prisma.CourtCreateNestedOneWithoutBookingsInput
|
court: Prisma.CourtCreateNestedOneWithoutBookingsInput
|
||||||
|
recurringGroup?: Prisma.RecurringBookingGroupCreateNestedOneWithoutBookingsInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CourtBookingUncheckedCreateInput = {
|
export type CourtBookingUncheckedCreateInput = {
|
||||||
@@ -324,7 +353,9 @@ export type CourtBookingUncheckedCreateInput = {
|
|||||||
endTime: string
|
endTime: string
|
||||||
customerName: string
|
customerName: string
|
||||||
customerPhone: string
|
customerPhone: string
|
||||||
|
customerEmail: string
|
||||||
status?: $Enums.CourtBookingStatus
|
status?: $Enums.CourtBookingStatus
|
||||||
|
recurringGroupId?: string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
}
|
}
|
||||||
@@ -337,10 +368,12 @@ export type CourtBookingUpdateInput = {
|
|||||||
endTime?: Prisma.StringFieldUpdateOperationsInput | string
|
endTime?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
customerName?: Prisma.StringFieldUpdateOperationsInput | string
|
customerName?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
customerPhone?: Prisma.StringFieldUpdateOperationsInput | string
|
customerPhone?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
customerEmail?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
status?: Prisma.EnumCourtBookingStatusFieldUpdateOperationsInput | $Enums.CourtBookingStatus
|
status?: Prisma.EnumCourtBookingStatusFieldUpdateOperationsInput | $Enums.CourtBookingStatus
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
court?: Prisma.CourtUpdateOneRequiredWithoutBookingsNestedInput
|
court?: Prisma.CourtUpdateOneRequiredWithoutBookingsNestedInput
|
||||||
|
recurringGroup?: Prisma.RecurringBookingGroupUpdateOneWithoutBookingsNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CourtBookingUncheckedUpdateInput = {
|
export type CourtBookingUncheckedUpdateInput = {
|
||||||
@@ -352,7 +385,9 @@ export type CourtBookingUncheckedUpdateInput = {
|
|||||||
endTime?: Prisma.StringFieldUpdateOperationsInput | string
|
endTime?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
customerName?: Prisma.StringFieldUpdateOperationsInput | string
|
customerName?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
customerPhone?: Prisma.StringFieldUpdateOperationsInput | string
|
customerPhone?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
customerEmail?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
status?: Prisma.EnumCourtBookingStatusFieldUpdateOperationsInput | $Enums.CourtBookingStatus
|
status?: Prisma.EnumCourtBookingStatusFieldUpdateOperationsInput | $Enums.CourtBookingStatus
|
||||||
|
recurringGroupId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
}
|
}
|
||||||
@@ -366,7 +401,9 @@ export type CourtBookingCreateManyInput = {
|
|||||||
endTime: string
|
endTime: string
|
||||||
customerName: string
|
customerName: string
|
||||||
customerPhone: string
|
customerPhone: string
|
||||||
|
customerEmail: string
|
||||||
status?: $Enums.CourtBookingStatus
|
status?: $Enums.CourtBookingStatus
|
||||||
|
recurringGroupId?: string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
}
|
}
|
||||||
@@ -379,6 +416,7 @@ export type CourtBookingUpdateManyMutationInput = {
|
|||||||
endTime?: Prisma.StringFieldUpdateOperationsInput | string
|
endTime?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
customerName?: Prisma.StringFieldUpdateOperationsInput | string
|
customerName?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
customerPhone?: Prisma.StringFieldUpdateOperationsInput | string
|
customerPhone?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
customerEmail?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
status?: Prisma.EnumCourtBookingStatusFieldUpdateOperationsInput | $Enums.CourtBookingStatus
|
status?: Prisma.EnumCourtBookingStatusFieldUpdateOperationsInput | $Enums.CourtBookingStatus
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
@@ -393,7 +431,9 @@ export type CourtBookingUncheckedUpdateManyInput = {
|
|||||||
endTime?: Prisma.StringFieldUpdateOperationsInput | string
|
endTime?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
customerName?: Prisma.StringFieldUpdateOperationsInput | string
|
customerName?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
customerPhone?: Prisma.StringFieldUpdateOperationsInput | string
|
customerPhone?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
customerEmail?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
status?: Prisma.EnumCourtBookingStatusFieldUpdateOperationsInput | $Enums.CourtBookingStatus
|
status?: Prisma.EnumCourtBookingStatusFieldUpdateOperationsInput | $Enums.CourtBookingStatus
|
||||||
|
recurringGroupId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
}
|
}
|
||||||
@@ -423,7 +463,9 @@ export type CourtBookingCountOrderByAggregateInput = {
|
|||||||
endTime?: Prisma.SortOrder
|
endTime?: Prisma.SortOrder
|
||||||
customerName?: Prisma.SortOrder
|
customerName?: Prisma.SortOrder
|
||||||
customerPhone?: Prisma.SortOrder
|
customerPhone?: Prisma.SortOrder
|
||||||
|
customerEmail?: Prisma.SortOrder
|
||||||
status?: Prisma.SortOrder
|
status?: Prisma.SortOrder
|
||||||
|
recurringGroupId?: Prisma.SortOrder
|
||||||
createdAt?: Prisma.SortOrder
|
createdAt?: Prisma.SortOrder
|
||||||
updatedAt?: Prisma.SortOrder
|
updatedAt?: Prisma.SortOrder
|
||||||
}
|
}
|
||||||
@@ -437,7 +479,9 @@ export type CourtBookingMaxOrderByAggregateInput = {
|
|||||||
endTime?: Prisma.SortOrder
|
endTime?: Prisma.SortOrder
|
||||||
customerName?: Prisma.SortOrder
|
customerName?: Prisma.SortOrder
|
||||||
customerPhone?: Prisma.SortOrder
|
customerPhone?: Prisma.SortOrder
|
||||||
|
customerEmail?: Prisma.SortOrder
|
||||||
status?: Prisma.SortOrder
|
status?: Prisma.SortOrder
|
||||||
|
recurringGroupId?: Prisma.SortOrder
|
||||||
createdAt?: Prisma.SortOrder
|
createdAt?: Prisma.SortOrder
|
||||||
updatedAt?: Prisma.SortOrder
|
updatedAt?: Prisma.SortOrder
|
||||||
}
|
}
|
||||||
@@ -451,7 +495,9 @@ export type CourtBookingMinOrderByAggregateInput = {
|
|||||||
endTime?: Prisma.SortOrder
|
endTime?: Prisma.SortOrder
|
||||||
customerName?: Prisma.SortOrder
|
customerName?: Prisma.SortOrder
|
||||||
customerPhone?: Prisma.SortOrder
|
customerPhone?: Prisma.SortOrder
|
||||||
|
customerEmail?: Prisma.SortOrder
|
||||||
status?: Prisma.SortOrder
|
status?: Prisma.SortOrder
|
||||||
|
recurringGroupId?: Prisma.SortOrder
|
||||||
createdAt?: Prisma.SortOrder
|
createdAt?: Prisma.SortOrder
|
||||||
updatedAt?: Prisma.SortOrder
|
updatedAt?: Prisma.SortOrder
|
||||||
}
|
}
|
||||||
@@ -502,6 +548,48 @@ export type EnumCourtBookingStatusFieldUpdateOperationsInput = {
|
|||||||
set?: $Enums.CourtBookingStatus
|
set?: $Enums.CourtBookingStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type CourtBookingCreateNestedManyWithoutRecurringGroupInput = {
|
||||||
|
create?: Prisma.XOR<Prisma.CourtBookingCreateWithoutRecurringGroupInput, Prisma.CourtBookingUncheckedCreateWithoutRecurringGroupInput> | Prisma.CourtBookingCreateWithoutRecurringGroupInput[] | Prisma.CourtBookingUncheckedCreateWithoutRecurringGroupInput[]
|
||||||
|
connectOrCreate?: Prisma.CourtBookingCreateOrConnectWithoutRecurringGroupInput | Prisma.CourtBookingCreateOrConnectWithoutRecurringGroupInput[]
|
||||||
|
createMany?: Prisma.CourtBookingCreateManyRecurringGroupInputEnvelope
|
||||||
|
connect?: Prisma.CourtBookingWhereUniqueInput | Prisma.CourtBookingWhereUniqueInput[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CourtBookingUncheckedCreateNestedManyWithoutRecurringGroupInput = {
|
||||||
|
create?: Prisma.XOR<Prisma.CourtBookingCreateWithoutRecurringGroupInput, Prisma.CourtBookingUncheckedCreateWithoutRecurringGroupInput> | Prisma.CourtBookingCreateWithoutRecurringGroupInput[] | Prisma.CourtBookingUncheckedCreateWithoutRecurringGroupInput[]
|
||||||
|
connectOrCreate?: Prisma.CourtBookingCreateOrConnectWithoutRecurringGroupInput | Prisma.CourtBookingCreateOrConnectWithoutRecurringGroupInput[]
|
||||||
|
createMany?: Prisma.CourtBookingCreateManyRecurringGroupInputEnvelope
|
||||||
|
connect?: Prisma.CourtBookingWhereUniqueInput | Prisma.CourtBookingWhereUniqueInput[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CourtBookingUpdateManyWithoutRecurringGroupNestedInput = {
|
||||||
|
create?: Prisma.XOR<Prisma.CourtBookingCreateWithoutRecurringGroupInput, Prisma.CourtBookingUncheckedCreateWithoutRecurringGroupInput> | Prisma.CourtBookingCreateWithoutRecurringGroupInput[] | Prisma.CourtBookingUncheckedCreateWithoutRecurringGroupInput[]
|
||||||
|
connectOrCreate?: Prisma.CourtBookingCreateOrConnectWithoutRecurringGroupInput | Prisma.CourtBookingCreateOrConnectWithoutRecurringGroupInput[]
|
||||||
|
upsert?: Prisma.CourtBookingUpsertWithWhereUniqueWithoutRecurringGroupInput | Prisma.CourtBookingUpsertWithWhereUniqueWithoutRecurringGroupInput[]
|
||||||
|
createMany?: Prisma.CourtBookingCreateManyRecurringGroupInputEnvelope
|
||||||
|
set?: Prisma.CourtBookingWhereUniqueInput | Prisma.CourtBookingWhereUniqueInput[]
|
||||||
|
disconnect?: Prisma.CourtBookingWhereUniqueInput | Prisma.CourtBookingWhereUniqueInput[]
|
||||||
|
delete?: Prisma.CourtBookingWhereUniqueInput | Prisma.CourtBookingWhereUniqueInput[]
|
||||||
|
connect?: Prisma.CourtBookingWhereUniqueInput | Prisma.CourtBookingWhereUniqueInput[]
|
||||||
|
update?: Prisma.CourtBookingUpdateWithWhereUniqueWithoutRecurringGroupInput | Prisma.CourtBookingUpdateWithWhereUniqueWithoutRecurringGroupInput[]
|
||||||
|
updateMany?: Prisma.CourtBookingUpdateManyWithWhereWithoutRecurringGroupInput | Prisma.CourtBookingUpdateManyWithWhereWithoutRecurringGroupInput[]
|
||||||
|
deleteMany?: Prisma.CourtBookingScalarWhereInput | Prisma.CourtBookingScalarWhereInput[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CourtBookingUncheckedUpdateManyWithoutRecurringGroupNestedInput = {
|
||||||
|
create?: Prisma.XOR<Prisma.CourtBookingCreateWithoutRecurringGroupInput, Prisma.CourtBookingUncheckedCreateWithoutRecurringGroupInput> | Prisma.CourtBookingCreateWithoutRecurringGroupInput[] | Prisma.CourtBookingUncheckedCreateWithoutRecurringGroupInput[]
|
||||||
|
connectOrCreate?: Prisma.CourtBookingCreateOrConnectWithoutRecurringGroupInput | Prisma.CourtBookingCreateOrConnectWithoutRecurringGroupInput[]
|
||||||
|
upsert?: Prisma.CourtBookingUpsertWithWhereUniqueWithoutRecurringGroupInput | Prisma.CourtBookingUpsertWithWhereUniqueWithoutRecurringGroupInput[]
|
||||||
|
createMany?: Prisma.CourtBookingCreateManyRecurringGroupInputEnvelope
|
||||||
|
set?: Prisma.CourtBookingWhereUniqueInput | Prisma.CourtBookingWhereUniqueInput[]
|
||||||
|
disconnect?: Prisma.CourtBookingWhereUniqueInput | Prisma.CourtBookingWhereUniqueInput[]
|
||||||
|
delete?: Prisma.CourtBookingWhereUniqueInput | Prisma.CourtBookingWhereUniqueInput[]
|
||||||
|
connect?: Prisma.CourtBookingWhereUniqueInput | Prisma.CourtBookingWhereUniqueInput[]
|
||||||
|
update?: Prisma.CourtBookingUpdateWithWhereUniqueWithoutRecurringGroupInput | Prisma.CourtBookingUpdateWithWhereUniqueWithoutRecurringGroupInput[]
|
||||||
|
updateMany?: Prisma.CourtBookingUpdateManyWithWhereWithoutRecurringGroupInput | Prisma.CourtBookingUpdateManyWithWhereWithoutRecurringGroupInput[]
|
||||||
|
deleteMany?: Prisma.CourtBookingScalarWhereInput | Prisma.CourtBookingScalarWhereInput[]
|
||||||
|
}
|
||||||
|
|
||||||
export type CourtBookingCreateWithoutCourtInput = {
|
export type CourtBookingCreateWithoutCourtInput = {
|
||||||
id: string
|
id: string
|
||||||
bookingCode: string
|
bookingCode: string
|
||||||
@@ -510,9 +598,11 @@ export type CourtBookingCreateWithoutCourtInput = {
|
|||||||
endTime: string
|
endTime: string
|
||||||
customerName: string
|
customerName: string
|
||||||
customerPhone: string
|
customerPhone: string
|
||||||
|
customerEmail: string
|
||||||
status?: $Enums.CourtBookingStatus
|
status?: $Enums.CourtBookingStatus
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
|
recurringGroup?: Prisma.RecurringBookingGroupCreateNestedOneWithoutBookingsInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CourtBookingUncheckedCreateWithoutCourtInput = {
|
export type CourtBookingUncheckedCreateWithoutCourtInput = {
|
||||||
@@ -523,7 +613,9 @@ export type CourtBookingUncheckedCreateWithoutCourtInput = {
|
|||||||
endTime: string
|
endTime: string
|
||||||
customerName: string
|
customerName: string
|
||||||
customerPhone: string
|
customerPhone: string
|
||||||
|
customerEmail: string
|
||||||
status?: $Enums.CourtBookingStatus
|
status?: $Enums.CourtBookingStatus
|
||||||
|
recurringGroupId?: string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
}
|
}
|
||||||
@@ -566,11 +658,69 @@ export type CourtBookingScalarWhereInput = {
|
|||||||
endTime?: Prisma.StringFilter<"CourtBooking"> | string
|
endTime?: Prisma.StringFilter<"CourtBooking"> | string
|
||||||
customerName?: Prisma.StringFilter<"CourtBooking"> | string
|
customerName?: Prisma.StringFilter<"CourtBooking"> | string
|
||||||
customerPhone?: Prisma.StringFilter<"CourtBooking"> | string
|
customerPhone?: Prisma.StringFilter<"CourtBooking"> | string
|
||||||
|
customerEmail?: Prisma.StringFilter<"CourtBooking"> | string
|
||||||
status?: Prisma.EnumCourtBookingStatusFilter<"CourtBooking"> | $Enums.CourtBookingStatus
|
status?: Prisma.EnumCourtBookingStatusFilter<"CourtBooking"> | $Enums.CourtBookingStatus
|
||||||
|
recurringGroupId?: Prisma.UuidNullableFilter<"CourtBooking"> | string | null
|
||||||
createdAt?: Prisma.DateTimeFilter<"CourtBooking"> | Date | string
|
createdAt?: Prisma.DateTimeFilter<"CourtBooking"> | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFilter<"CourtBooking"> | Date | string
|
updatedAt?: Prisma.DateTimeFilter<"CourtBooking"> | Date | string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type CourtBookingCreateWithoutRecurringGroupInput = {
|
||||||
|
id: string
|
||||||
|
bookingCode: string
|
||||||
|
bookingDate: Date | string
|
||||||
|
startTime: string
|
||||||
|
endTime: string
|
||||||
|
customerName: string
|
||||||
|
customerPhone: string
|
||||||
|
customerEmail: string
|
||||||
|
status?: $Enums.CourtBookingStatus
|
||||||
|
createdAt?: Date | string
|
||||||
|
updatedAt?: Date | string
|
||||||
|
court: Prisma.CourtCreateNestedOneWithoutBookingsInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CourtBookingUncheckedCreateWithoutRecurringGroupInput = {
|
||||||
|
id: string
|
||||||
|
bookingCode: string
|
||||||
|
courtId: string
|
||||||
|
bookingDate: Date | string
|
||||||
|
startTime: string
|
||||||
|
endTime: string
|
||||||
|
customerName: string
|
||||||
|
customerPhone: string
|
||||||
|
customerEmail: string
|
||||||
|
status?: $Enums.CourtBookingStatus
|
||||||
|
createdAt?: Date | string
|
||||||
|
updatedAt?: Date | string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CourtBookingCreateOrConnectWithoutRecurringGroupInput = {
|
||||||
|
where: Prisma.CourtBookingWhereUniqueInput
|
||||||
|
create: Prisma.XOR<Prisma.CourtBookingCreateWithoutRecurringGroupInput, Prisma.CourtBookingUncheckedCreateWithoutRecurringGroupInput>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CourtBookingCreateManyRecurringGroupInputEnvelope = {
|
||||||
|
data: Prisma.CourtBookingCreateManyRecurringGroupInput | Prisma.CourtBookingCreateManyRecurringGroupInput[]
|
||||||
|
skipDuplicates?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CourtBookingUpsertWithWhereUniqueWithoutRecurringGroupInput = {
|
||||||
|
where: Prisma.CourtBookingWhereUniqueInput
|
||||||
|
update: Prisma.XOR<Prisma.CourtBookingUpdateWithoutRecurringGroupInput, Prisma.CourtBookingUncheckedUpdateWithoutRecurringGroupInput>
|
||||||
|
create: Prisma.XOR<Prisma.CourtBookingCreateWithoutRecurringGroupInput, Prisma.CourtBookingUncheckedCreateWithoutRecurringGroupInput>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CourtBookingUpdateWithWhereUniqueWithoutRecurringGroupInput = {
|
||||||
|
where: Prisma.CourtBookingWhereUniqueInput
|
||||||
|
data: Prisma.XOR<Prisma.CourtBookingUpdateWithoutRecurringGroupInput, Prisma.CourtBookingUncheckedUpdateWithoutRecurringGroupInput>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CourtBookingUpdateManyWithWhereWithoutRecurringGroupInput = {
|
||||||
|
where: Prisma.CourtBookingScalarWhereInput
|
||||||
|
data: Prisma.XOR<Prisma.CourtBookingUpdateManyMutationInput, Prisma.CourtBookingUncheckedUpdateManyWithoutRecurringGroupInput>
|
||||||
|
}
|
||||||
|
|
||||||
export type CourtBookingCreateManyCourtInput = {
|
export type CourtBookingCreateManyCourtInput = {
|
||||||
id: string
|
id: string
|
||||||
bookingCode: string
|
bookingCode: string
|
||||||
@@ -579,7 +729,9 @@ export type CourtBookingCreateManyCourtInput = {
|
|||||||
endTime: string
|
endTime: string
|
||||||
customerName: string
|
customerName: string
|
||||||
customerPhone: string
|
customerPhone: string
|
||||||
|
customerEmail: string
|
||||||
status?: $Enums.CourtBookingStatus
|
status?: $Enums.CourtBookingStatus
|
||||||
|
recurringGroupId?: string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
}
|
}
|
||||||
@@ -592,9 +744,11 @@ export type CourtBookingUpdateWithoutCourtInput = {
|
|||||||
endTime?: Prisma.StringFieldUpdateOperationsInput | string
|
endTime?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
customerName?: Prisma.StringFieldUpdateOperationsInput | string
|
customerName?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
customerPhone?: Prisma.StringFieldUpdateOperationsInput | string
|
customerPhone?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
customerEmail?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
status?: Prisma.EnumCourtBookingStatusFieldUpdateOperationsInput | $Enums.CourtBookingStatus
|
status?: Prisma.EnumCourtBookingStatusFieldUpdateOperationsInput | $Enums.CourtBookingStatus
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
recurringGroup?: Prisma.RecurringBookingGroupUpdateOneWithoutBookingsNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CourtBookingUncheckedUpdateWithoutCourtInput = {
|
export type CourtBookingUncheckedUpdateWithoutCourtInput = {
|
||||||
@@ -605,7 +759,9 @@ export type CourtBookingUncheckedUpdateWithoutCourtInput = {
|
|||||||
endTime?: Prisma.StringFieldUpdateOperationsInput | string
|
endTime?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
customerName?: Prisma.StringFieldUpdateOperationsInput | string
|
customerName?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
customerPhone?: Prisma.StringFieldUpdateOperationsInput | string
|
customerPhone?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
customerEmail?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
status?: Prisma.EnumCourtBookingStatusFieldUpdateOperationsInput | $Enums.CourtBookingStatus
|
status?: Prisma.EnumCourtBookingStatusFieldUpdateOperationsInput | $Enums.CourtBookingStatus
|
||||||
|
recurringGroupId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
}
|
}
|
||||||
@@ -618,6 +774,68 @@ export type CourtBookingUncheckedUpdateManyWithoutCourtInput = {
|
|||||||
endTime?: Prisma.StringFieldUpdateOperationsInput | string
|
endTime?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
customerName?: Prisma.StringFieldUpdateOperationsInput | string
|
customerName?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
customerPhone?: Prisma.StringFieldUpdateOperationsInput | string
|
customerPhone?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
customerEmail?: Prisma.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
|
status?: Prisma.EnumCourtBookingStatusFieldUpdateOperationsInput | $Enums.CourtBookingStatus
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
@@ -634,10 +852,13 @@ export type CourtBookingSelect<ExtArgs extends runtime.Types.Extensions.Internal
|
|||||||
endTime?: boolean
|
endTime?: boolean
|
||||||
customerName?: boolean
|
customerName?: boolean
|
||||||
customerPhone?: boolean
|
customerPhone?: boolean
|
||||||
|
customerEmail?: boolean
|
||||||
status?: boolean
|
status?: boolean
|
||||||
|
recurringGroupId?: boolean
|
||||||
createdAt?: boolean
|
createdAt?: boolean
|
||||||
updatedAt?: boolean
|
updatedAt?: boolean
|
||||||
court?: boolean | Prisma.CourtDefaultArgs<ExtArgs>
|
court?: boolean | Prisma.CourtDefaultArgs<ExtArgs>
|
||||||
|
recurringGroup?: boolean | Prisma.CourtBooking$recurringGroupArgs<ExtArgs>
|
||||||
}, ExtArgs["result"]["courtBooking"]>
|
}, ExtArgs["result"]["courtBooking"]>
|
||||||
|
|
||||||
export type CourtBookingSelectCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
|
export type CourtBookingSelectCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
|
||||||
@@ -649,10 +870,13 @@ export type CourtBookingSelectCreateManyAndReturn<ExtArgs extends runtime.Types.
|
|||||||
endTime?: boolean
|
endTime?: boolean
|
||||||
customerName?: boolean
|
customerName?: boolean
|
||||||
customerPhone?: boolean
|
customerPhone?: boolean
|
||||||
|
customerEmail?: boolean
|
||||||
status?: boolean
|
status?: boolean
|
||||||
|
recurringGroupId?: boolean
|
||||||
createdAt?: boolean
|
createdAt?: boolean
|
||||||
updatedAt?: boolean
|
updatedAt?: boolean
|
||||||
court?: boolean | Prisma.CourtDefaultArgs<ExtArgs>
|
court?: boolean | Prisma.CourtDefaultArgs<ExtArgs>
|
||||||
|
recurringGroup?: boolean | Prisma.CourtBooking$recurringGroupArgs<ExtArgs>
|
||||||
}, ExtArgs["result"]["courtBooking"]>
|
}, ExtArgs["result"]["courtBooking"]>
|
||||||
|
|
||||||
export type CourtBookingSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
|
export type CourtBookingSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
|
||||||
@@ -664,10 +888,13 @@ export type CourtBookingSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.
|
|||||||
endTime?: boolean
|
endTime?: boolean
|
||||||
customerName?: boolean
|
customerName?: boolean
|
||||||
customerPhone?: boolean
|
customerPhone?: boolean
|
||||||
|
customerEmail?: boolean
|
||||||
status?: boolean
|
status?: boolean
|
||||||
|
recurringGroupId?: boolean
|
||||||
createdAt?: boolean
|
createdAt?: boolean
|
||||||
updatedAt?: boolean
|
updatedAt?: boolean
|
||||||
court?: boolean | Prisma.CourtDefaultArgs<ExtArgs>
|
court?: boolean | Prisma.CourtDefaultArgs<ExtArgs>
|
||||||
|
recurringGroup?: boolean | Prisma.CourtBooking$recurringGroupArgs<ExtArgs>
|
||||||
}, ExtArgs["result"]["courtBooking"]>
|
}, ExtArgs["result"]["courtBooking"]>
|
||||||
|
|
||||||
export type CourtBookingSelectScalar = {
|
export type CourtBookingSelectScalar = {
|
||||||
@@ -679,26 +906,32 @@ export type CourtBookingSelectScalar = {
|
|||||||
endTime?: boolean
|
endTime?: boolean
|
||||||
customerName?: boolean
|
customerName?: boolean
|
||||||
customerPhone?: boolean
|
customerPhone?: boolean
|
||||||
|
customerEmail?: boolean
|
||||||
status?: boolean
|
status?: boolean
|
||||||
|
recurringGroupId?: boolean
|
||||||
createdAt?: boolean
|
createdAt?: boolean
|
||||||
updatedAt?: boolean
|
updatedAt?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CourtBookingOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "bookingCode" | "courtId" | "bookingDate" | "startTime" | "endTime" | "customerName" | "customerPhone" | "status" | "createdAt" | "updatedAt", ExtArgs["result"]["courtBooking"]>
|
export type CourtBookingOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "bookingCode" | "courtId" | "bookingDate" | "startTime" | "endTime" | "customerName" | "customerPhone" | "customerEmail" | "status" | "recurringGroupId" | "createdAt" | "updatedAt", ExtArgs["result"]["courtBooking"]>
|
||||||
export type CourtBookingInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
export type CourtBookingInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
court?: boolean | Prisma.CourtDefaultArgs<ExtArgs>
|
court?: boolean | Prisma.CourtDefaultArgs<ExtArgs>
|
||||||
|
recurringGroup?: boolean | Prisma.CourtBooking$recurringGroupArgs<ExtArgs>
|
||||||
}
|
}
|
||||||
export type CourtBookingIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
export type CourtBookingIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
court?: boolean | Prisma.CourtDefaultArgs<ExtArgs>
|
court?: boolean | Prisma.CourtDefaultArgs<ExtArgs>
|
||||||
|
recurringGroup?: boolean | Prisma.CourtBooking$recurringGroupArgs<ExtArgs>
|
||||||
}
|
}
|
||||||
export type CourtBookingIncludeUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
export type CourtBookingIncludeUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
court?: boolean | Prisma.CourtDefaultArgs<ExtArgs>
|
court?: boolean | Prisma.CourtDefaultArgs<ExtArgs>
|
||||||
|
recurringGroup?: boolean | Prisma.CourtBooking$recurringGroupArgs<ExtArgs>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type $CourtBookingPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
export type $CourtBookingPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
name: "CourtBooking"
|
name: "CourtBooking"
|
||||||
objects: {
|
objects: {
|
||||||
court: Prisma.$CourtPayload<ExtArgs>
|
court: Prisma.$CourtPayload<ExtArgs>
|
||||||
|
recurringGroup: Prisma.$RecurringBookingGroupPayload<ExtArgs> | null
|
||||||
}
|
}
|
||||||
scalars: runtime.Types.Extensions.GetPayloadResult<{
|
scalars: runtime.Types.Extensions.GetPayloadResult<{
|
||||||
id: string
|
id: string
|
||||||
@@ -709,7 +942,9 @@ export type $CourtBookingPayload<ExtArgs extends runtime.Types.Extensions.Intern
|
|||||||
endTime: string
|
endTime: string
|
||||||
customerName: string
|
customerName: string
|
||||||
customerPhone: string
|
customerPhone: string
|
||||||
|
customerEmail: string
|
||||||
status: $Enums.CourtBookingStatus
|
status: $Enums.CourtBookingStatus
|
||||||
|
recurringGroupId: string | null
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
updatedAt: Date
|
updatedAt: Date
|
||||||
}, ExtArgs["result"]["courtBooking"]>
|
}, ExtArgs["result"]["courtBooking"]>
|
||||||
@@ -1107,6 +1342,7 @@ readonly fields: CourtBookingFieldRefs;
|
|||||||
export interface Prisma__CourtBookingClient<T, Null = never, ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs, GlobalOmitOptions = {}> extends Prisma.PrismaPromise<T> {
|
export interface Prisma__CourtBookingClient<T, Null = never, ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs, GlobalOmitOptions = {}> extends Prisma.PrismaPromise<T> {
|
||||||
readonly [Symbol.toStringTag]: "PrismaPromise"
|
readonly [Symbol.toStringTag]: "PrismaPromise"
|
||||||
court<T extends Prisma.CourtDefaultArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.CourtDefaultArgs<ExtArgs>>): Prisma.Prisma__CourtClient<runtime.Types.Result.GetResult<Prisma.$CourtPayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | Null, Null, ExtArgs, GlobalOmitOptions>
|
court<T extends Prisma.CourtDefaultArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.CourtDefaultArgs<ExtArgs>>): Prisma.Prisma__CourtClient<runtime.Types.Result.GetResult<Prisma.$CourtPayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | Null, Null, ExtArgs, GlobalOmitOptions>
|
||||||
|
recurringGroup<T extends Prisma.CourtBooking$recurringGroupArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.CourtBooking$recurringGroupArgs<ExtArgs>>): Prisma.Prisma__RecurringBookingGroupClient<runtime.Types.Result.GetResult<Prisma.$RecurringBookingGroupPayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions>
|
||||||
/**
|
/**
|
||||||
* Attaches callbacks for the resolution and/or rejection of the Promise.
|
* Attaches callbacks for the resolution and/or rejection of the Promise.
|
||||||
* @param onfulfilled The callback to execute when the Promise is resolved.
|
* @param onfulfilled The callback to execute when the Promise is resolved.
|
||||||
@@ -1144,7 +1380,9 @@ export interface CourtBookingFieldRefs {
|
|||||||
readonly endTime: Prisma.FieldRef<"CourtBooking", 'String'>
|
readonly endTime: Prisma.FieldRef<"CourtBooking", 'String'>
|
||||||
readonly customerName: Prisma.FieldRef<"CourtBooking", 'String'>
|
readonly customerName: Prisma.FieldRef<"CourtBooking", 'String'>
|
||||||
readonly customerPhone: Prisma.FieldRef<"CourtBooking", 'String'>
|
readonly customerPhone: Prisma.FieldRef<"CourtBooking", 'String'>
|
||||||
|
readonly customerEmail: Prisma.FieldRef<"CourtBooking", 'String'>
|
||||||
readonly status: Prisma.FieldRef<"CourtBooking", 'CourtBookingStatus'>
|
readonly status: Prisma.FieldRef<"CourtBooking", 'CourtBookingStatus'>
|
||||||
|
readonly recurringGroupId: Prisma.FieldRef<"CourtBooking", 'String'>
|
||||||
readonly createdAt: Prisma.FieldRef<"CourtBooking", 'DateTime'>
|
readonly createdAt: Prisma.FieldRef<"CourtBooking", 'DateTime'>
|
||||||
readonly updatedAt: Prisma.FieldRef<"CourtBooking", 'DateTime'>
|
readonly updatedAt: Prisma.FieldRef<"CourtBooking", 'DateTime'>
|
||||||
}
|
}
|
||||||
@@ -1547,6 +1785,25 @@ export type CourtBookingDeleteManyArgs<ExtArgs extends runtime.Types.Extensions.
|
|||||||
limit?: number
|
limit?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CourtBooking.recurringGroup
|
||||||
|
*/
|
||||||
|
export type CourtBooking$recurringGroupArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
|
/**
|
||||||
|
* Select specific fields to fetch from the RecurringBookingGroup
|
||||||
|
*/
|
||||||
|
select?: Prisma.RecurringBookingGroupSelect<ExtArgs> | null
|
||||||
|
/**
|
||||||
|
* Omit specific fields from the RecurringBookingGroup
|
||||||
|
*/
|
||||||
|
omit?: Prisma.RecurringBookingGroupOmit<ExtArgs> | null
|
||||||
|
/**
|
||||||
|
* Choose, which related nodes to fetch as well
|
||||||
|
*/
|
||||||
|
include?: Prisma.RecurringBookingGroupInclude<ExtArgs> | null
|
||||||
|
where?: Prisma.RecurringBookingGroupWhereInput
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CourtBooking without action
|
* CourtBooking without action
|
||||||
*/
|
*/
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -31,6 +31,9 @@ export type UserMinAggregateOutputType = {
|
|||||||
emailVerified: boolean | null
|
emailVerified: boolean | null
|
||||||
image: string | null
|
image: string | null
|
||||||
phone: string | null
|
phone: string | null
|
||||||
|
banned: boolean | null
|
||||||
|
bannedAt: Date | null
|
||||||
|
banReason: string | null
|
||||||
createdAt: Date | null
|
createdAt: Date | null
|
||||||
updatedAt: Date | null
|
updatedAt: Date | null
|
||||||
role: string | null
|
role: string | null
|
||||||
@@ -43,6 +46,9 @@ export type UserMaxAggregateOutputType = {
|
|||||||
emailVerified: boolean | null
|
emailVerified: boolean | null
|
||||||
image: string | null
|
image: string | null
|
||||||
phone: string | null
|
phone: string | null
|
||||||
|
banned: boolean | null
|
||||||
|
bannedAt: Date | null
|
||||||
|
banReason: string | null
|
||||||
createdAt: Date | null
|
createdAt: Date | null
|
||||||
updatedAt: Date | null
|
updatedAt: Date | null
|
||||||
role: string | null
|
role: string | null
|
||||||
@@ -55,6 +61,9 @@ export type UserCountAggregateOutputType = {
|
|||||||
emailVerified: number
|
emailVerified: number
|
||||||
image: number
|
image: number
|
||||||
phone: number
|
phone: number
|
||||||
|
banned: number
|
||||||
|
bannedAt: number
|
||||||
|
banReason: number
|
||||||
createdAt: number
|
createdAt: number
|
||||||
updatedAt: number
|
updatedAt: number
|
||||||
role: number
|
role: number
|
||||||
@@ -69,6 +78,9 @@ export type UserMinAggregateInputType = {
|
|||||||
emailVerified?: true
|
emailVerified?: true
|
||||||
image?: true
|
image?: true
|
||||||
phone?: true
|
phone?: true
|
||||||
|
banned?: true
|
||||||
|
bannedAt?: true
|
||||||
|
banReason?: true
|
||||||
createdAt?: true
|
createdAt?: true
|
||||||
updatedAt?: true
|
updatedAt?: true
|
||||||
role?: true
|
role?: true
|
||||||
@@ -81,6 +93,9 @@ export type UserMaxAggregateInputType = {
|
|||||||
emailVerified?: true
|
emailVerified?: true
|
||||||
image?: true
|
image?: true
|
||||||
phone?: true
|
phone?: true
|
||||||
|
banned?: true
|
||||||
|
bannedAt?: true
|
||||||
|
banReason?: true
|
||||||
createdAt?: true
|
createdAt?: true
|
||||||
updatedAt?: true
|
updatedAt?: true
|
||||||
role?: true
|
role?: true
|
||||||
@@ -93,6 +108,9 @@ export type UserCountAggregateInputType = {
|
|||||||
emailVerified?: true
|
emailVerified?: true
|
||||||
image?: true
|
image?: true
|
||||||
phone?: true
|
phone?: true
|
||||||
|
banned?: true
|
||||||
|
bannedAt?: true
|
||||||
|
banReason?: true
|
||||||
createdAt?: true
|
createdAt?: true
|
||||||
updatedAt?: true
|
updatedAt?: true
|
||||||
role?: true
|
role?: true
|
||||||
@@ -178,6 +196,9 @@ export type UserGroupByOutputType = {
|
|||||||
emailVerified: boolean
|
emailVerified: boolean
|
||||||
image: string | null
|
image: string | null
|
||||||
phone: string | null
|
phone: string | null
|
||||||
|
banned: boolean
|
||||||
|
bannedAt: Date | null
|
||||||
|
banReason: string | null
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
updatedAt: Date
|
updatedAt: Date
|
||||||
role: string
|
role: string
|
||||||
@@ -211,6 +232,9 @@ export type UserWhereInput = {
|
|||||||
emailVerified?: Prisma.BoolFilter<"User"> | boolean
|
emailVerified?: Prisma.BoolFilter<"User"> | boolean
|
||||||
image?: Prisma.StringNullableFilter<"User"> | string | null
|
image?: Prisma.StringNullableFilter<"User"> | string | null
|
||||||
phone?: Prisma.StringNullableFilter<"User"> | string | null
|
phone?: Prisma.StringNullableFilter<"User"> | string | null
|
||||||
|
banned?: Prisma.BoolFilter<"User"> | boolean
|
||||||
|
bannedAt?: Prisma.DateTimeNullableFilter<"User"> | Date | string | null
|
||||||
|
banReason?: Prisma.StringNullableFilter<"User"> | string | null
|
||||||
createdAt?: Prisma.DateTimeFilter<"User"> | Date | string
|
createdAt?: Prisma.DateTimeFilter<"User"> | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFilter<"User"> | Date | string
|
updatedAt?: Prisma.DateTimeFilter<"User"> | Date | string
|
||||||
role?: Prisma.StringFilter<"User"> | string
|
role?: Prisma.StringFilter<"User"> | string
|
||||||
@@ -226,6 +250,9 @@ export type UserOrderByWithRelationInput = {
|
|||||||
emailVerified?: Prisma.SortOrder
|
emailVerified?: Prisma.SortOrder
|
||||||
image?: Prisma.SortOrderInput | Prisma.SortOrder
|
image?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
phone?: Prisma.SortOrderInput | Prisma.SortOrder
|
phone?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
|
banned?: Prisma.SortOrder
|
||||||
|
bannedAt?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
|
banReason?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
createdAt?: Prisma.SortOrder
|
createdAt?: Prisma.SortOrder
|
||||||
updatedAt?: Prisma.SortOrder
|
updatedAt?: Prisma.SortOrder
|
||||||
role?: Prisma.SortOrder
|
role?: Prisma.SortOrder
|
||||||
@@ -244,6 +271,9 @@ export type UserWhereUniqueInput = Prisma.AtLeast<{
|
|||||||
emailVerified?: Prisma.BoolFilter<"User"> | boolean
|
emailVerified?: Prisma.BoolFilter<"User"> | boolean
|
||||||
image?: Prisma.StringNullableFilter<"User"> | string | null
|
image?: Prisma.StringNullableFilter<"User"> | string | null
|
||||||
phone?: Prisma.StringNullableFilter<"User"> | string | null
|
phone?: Prisma.StringNullableFilter<"User"> | string | null
|
||||||
|
banned?: Prisma.BoolFilter<"User"> | boolean
|
||||||
|
bannedAt?: Prisma.DateTimeNullableFilter<"User"> | Date | string | null
|
||||||
|
banReason?: Prisma.StringNullableFilter<"User"> | string | null
|
||||||
createdAt?: Prisma.DateTimeFilter<"User"> | Date | string
|
createdAt?: Prisma.DateTimeFilter<"User"> | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFilter<"User"> | Date | string
|
updatedAt?: Prisma.DateTimeFilter<"User"> | Date | string
|
||||||
role?: Prisma.StringFilter<"User"> | string
|
role?: Prisma.StringFilter<"User"> | string
|
||||||
@@ -259,6 +289,9 @@ export type UserOrderByWithAggregationInput = {
|
|||||||
emailVerified?: Prisma.SortOrder
|
emailVerified?: Prisma.SortOrder
|
||||||
image?: Prisma.SortOrderInput | Prisma.SortOrder
|
image?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
phone?: Prisma.SortOrderInput | Prisma.SortOrder
|
phone?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
|
banned?: Prisma.SortOrder
|
||||||
|
bannedAt?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
|
banReason?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
createdAt?: Prisma.SortOrder
|
createdAt?: Prisma.SortOrder
|
||||||
updatedAt?: Prisma.SortOrder
|
updatedAt?: Prisma.SortOrder
|
||||||
role?: Prisma.SortOrder
|
role?: Prisma.SortOrder
|
||||||
@@ -277,6 +310,9 @@ export type UserScalarWhereWithAggregatesInput = {
|
|||||||
emailVerified?: Prisma.BoolWithAggregatesFilter<"User"> | boolean
|
emailVerified?: Prisma.BoolWithAggregatesFilter<"User"> | boolean
|
||||||
image?: Prisma.StringNullableWithAggregatesFilter<"User"> | string | null
|
image?: Prisma.StringNullableWithAggregatesFilter<"User"> | string | null
|
||||||
phone?: Prisma.StringNullableWithAggregatesFilter<"User"> | string | null
|
phone?: Prisma.StringNullableWithAggregatesFilter<"User"> | string | null
|
||||||
|
banned?: Prisma.BoolWithAggregatesFilter<"User"> | boolean
|
||||||
|
bannedAt?: Prisma.DateTimeNullableWithAggregatesFilter<"User"> | Date | string | null
|
||||||
|
banReason?: Prisma.StringNullableWithAggregatesFilter<"User"> | string | null
|
||||||
createdAt?: Prisma.DateTimeWithAggregatesFilter<"User"> | Date | string
|
createdAt?: Prisma.DateTimeWithAggregatesFilter<"User"> | Date | string
|
||||||
updatedAt?: Prisma.DateTimeWithAggregatesFilter<"User"> | Date | string
|
updatedAt?: Prisma.DateTimeWithAggregatesFilter<"User"> | Date | string
|
||||||
role?: Prisma.StringWithAggregatesFilter<"User"> | string
|
role?: Prisma.StringWithAggregatesFilter<"User"> | string
|
||||||
@@ -289,6 +325,9 @@ export type UserCreateInput = {
|
|||||||
emailVerified?: boolean
|
emailVerified?: boolean
|
||||||
image?: string | null
|
image?: string | null
|
||||||
phone?: string | null
|
phone?: string | null
|
||||||
|
banned?: boolean
|
||||||
|
bannedAt?: Date | string | null
|
||||||
|
banReason?: string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
role?: string
|
role?: string
|
||||||
@@ -304,6 +343,9 @@ export type UserUncheckedCreateInput = {
|
|||||||
emailVerified?: boolean
|
emailVerified?: boolean
|
||||||
image?: string | null
|
image?: string | null
|
||||||
phone?: string | null
|
phone?: string | null
|
||||||
|
banned?: boolean
|
||||||
|
bannedAt?: Date | string | null
|
||||||
|
banReason?: string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
role?: string
|
role?: string
|
||||||
@@ -319,6 +361,9 @@ export type UserUpdateInput = {
|
|||||||
emailVerified?: Prisma.BoolFieldUpdateOperationsInput | boolean
|
emailVerified?: Prisma.BoolFieldUpdateOperationsInput | boolean
|
||||||
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
phone?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
phone?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
banned?: Prisma.BoolFieldUpdateOperationsInput | boolean
|
||||||
|
bannedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
|
banReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
role?: Prisma.StringFieldUpdateOperationsInput | string
|
role?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
@@ -334,6 +379,9 @@ export type UserUncheckedUpdateInput = {
|
|||||||
emailVerified?: Prisma.BoolFieldUpdateOperationsInput | boolean
|
emailVerified?: Prisma.BoolFieldUpdateOperationsInput | boolean
|
||||||
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
phone?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
phone?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
banned?: Prisma.BoolFieldUpdateOperationsInput | boolean
|
||||||
|
bannedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
|
banReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
role?: Prisma.StringFieldUpdateOperationsInput | string
|
role?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
@@ -349,6 +397,9 @@ export type UserCreateManyInput = {
|
|||||||
emailVerified?: boolean
|
emailVerified?: boolean
|
||||||
image?: string | null
|
image?: string | null
|
||||||
phone?: string | null
|
phone?: string | null
|
||||||
|
banned?: boolean
|
||||||
|
bannedAt?: Date | string | null
|
||||||
|
banReason?: string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
role?: string
|
role?: string
|
||||||
@@ -361,6 +412,9 @@ export type UserUpdateManyMutationInput = {
|
|||||||
emailVerified?: Prisma.BoolFieldUpdateOperationsInput | boolean
|
emailVerified?: Prisma.BoolFieldUpdateOperationsInput | boolean
|
||||||
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
phone?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
phone?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
banned?: Prisma.BoolFieldUpdateOperationsInput | boolean
|
||||||
|
bannedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
|
banReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
role?: Prisma.StringFieldUpdateOperationsInput | string
|
role?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
@@ -373,6 +427,9 @@ export type UserUncheckedUpdateManyInput = {
|
|||||||
emailVerified?: Prisma.BoolFieldUpdateOperationsInput | boolean
|
emailVerified?: Prisma.BoolFieldUpdateOperationsInput | boolean
|
||||||
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
phone?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
phone?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
banned?: Prisma.BoolFieldUpdateOperationsInput | boolean
|
||||||
|
bannedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
|
banReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
role?: Prisma.StringFieldUpdateOperationsInput | string
|
role?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
@@ -385,6 +442,9 @@ export type UserCountOrderByAggregateInput = {
|
|||||||
emailVerified?: Prisma.SortOrder
|
emailVerified?: Prisma.SortOrder
|
||||||
image?: Prisma.SortOrder
|
image?: Prisma.SortOrder
|
||||||
phone?: Prisma.SortOrder
|
phone?: Prisma.SortOrder
|
||||||
|
banned?: Prisma.SortOrder
|
||||||
|
bannedAt?: Prisma.SortOrder
|
||||||
|
banReason?: Prisma.SortOrder
|
||||||
createdAt?: Prisma.SortOrder
|
createdAt?: Prisma.SortOrder
|
||||||
updatedAt?: Prisma.SortOrder
|
updatedAt?: Prisma.SortOrder
|
||||||
role?: Prisma.SortOrder
|
role?: Prisma.SortOrder
|
||||||
@@ -397,6 +457,9 @@ export type UserMaxOrderByAggregateInput = {
|
|||||||
emailVerified?: Prisma.SortOrder
|
emailVerified?: Prisma.SortOrder
|
||||||
image?: Prisma.SortOrder
|
image?: Prisma.SortOrder
|
||||||
phone?: Prisma.SortOrder
|
phone?: Prisma.SortOrder
|
||||||
|
banned?: Prisma.SortOrder
|
||||||
|
bannedAt?: Prisma.SortOrder
|
||||||
|
banReason?: Prisma.SortOrder
|
||||||
createdAt?: Prisma.SortOrder
|
createdAt?: Prisma.SortOrder
|
||||||
updatedAt?: Prisma.SortOrder
|
updatedAt?: Prisma.SortOrder
|
||||||
role?: Prisma.SortOrder
|
role?: Prisma.SortOrder
|
||||||
@@ -409,6 +472,9 @@ export type UserMinOrderByAggregateInput = {
|
|||||||
emailVerified?: Prisma.SortOrder
|
emailVerified?: Prisma.SortOrder
|
||||||
image?: Prisma.SortOrder
|
image?: Prisma.SortOrder
|
||||||
phone?: Prisma.SortOrder
|
phone?: Prisma.SortOrder
|
||||||
|
banned?: Prisma.SortOrder
|
||||||
|
bannedAt?: Prisma.SortOrder
|
||||||
|
banReason?: Prisma.SortOrder
|
||||||
createdAt?: Prisma.SortOrder
|
createdAt?: Prisma.SortOrder
|
||||||
updatedAt?: Prisma.SortOrder
|
updatedAt?: Prisma.SortOrder
|
||||||
role?: Prisma.SortOrder
|
role?: Prisma.SortOrder
|
||||||
@@ -431,6 +497,10 @@ export type NullableStringFieldUpdateOperationsInput = {
|
|||||||
set?: string | null
|
set?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type NullableDateTimeFieldUpdateOperationsInput = {
|
||||||
|
set?: Date | string | null
|
||||||
|
}
|
||||||
|
|
||||||
export type DateTimeFieldUpdateOperationsInput = {
|
export type DateTimeFieldUpdateOperationsInput = {
|
||||||
set?: Date | string
|
set?: Date | string
|
||||||
}
|
}
|
||||||
@@ -484,6 +554,9 @@ export type UserCreateWithoutSessionsInput = {
|
|||||||
emailVerified?: boolean
|
emailVerified?: boolean
|
||||||
image?: string | null
|
image?: string | null
|
||||||
phone?: string | null
|
phone?: string | null
|
||||||
|
banned?: boolean
|
||||||
|
bannedAt?: Date | string | null
|
||||||
|
banReason?: string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
role?: string
|
role?: string
|
||||||
@@ -498,6 +571,9 @@ export type UserUncheckedCreateWithoutSessionsInput = {
|
|||||||
emailVerified?: boolean
|
emailVerified?: boolean
|
||||||
image?: string | null
|
image?: string | null
|
||||||
phone?: string | null
|
phone?: string | null
|
||||||
|
banned?: boolean
|
||||||
|
bannedAt?: Date | string | null
|
||||||
|
banReason?: string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
role?: string
|
role?: string
|
||||||
@@ -528,6 +604,9 @@ export type UserUpdateWithoutSessionsInput = {
|
|||||||
emailVerified?: Prisma.BoolFieldUpdateOperationsInput | boolean
|
emailVerified?: Prisma.BoolFieldUpdateOperationsInput | boolean
|
||||||
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
phone?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
phone?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
banned?: Prisma.BoolFieldUpdateOperationsInput | boolean
|
||||||
|
bannedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
|
banReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
role?: Prisma.StringFieldUpdateOperationsInput | string
|
role?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
@@ -542,6 +621,9 @@ export type UserUncheckedUpdateWithoutSessionsInput = {
|
|||||||
emailVerified?: Prisma.BoolFieldUpdateOperationsInput | boolean
|
emailVerified?: Prisma.BoolFieldUpdateOperationsInput | boolean
|
||||||
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
phone?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
phone?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
banned?: Prisma.BoolFieldUpdateOperationsInput | boolean
|
||||||
|
bannedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
|
banReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
role?: Prisma.StringFieldUpdateOperationsInput | string
|
role?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
@@ -556,6 +638,9 @@ export type UserCreateWithoutAccountsInput = {
|
|||||||
emailVerified?: boolean
|
emailVerified?: boolean
|
||||||
image?: string | null
|
image?: string | null
|
||||||
phone?: string | null
|
phone?: string | null
|
||||||
|
banned?: boolean
|
||||||
|
bannedAt?: Date | string | null
|
||||||
|
banReason?: string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
role?: string
|
role?: string
|
||||||
@@ -570,6 +655,9 @@ export type UserUncheckedCreateWithoutAccountsInput = {
|
|||||||
emailVerified?: boolean
|
emailVerified?: boolean
|
||||||
image?: string | null
|
image?: string | null
|
||||||
phone?: string | null
|
phone?: string | null
|
||||||
|
banned?: boolean
|
||||||
|
bannedAt?: Date | string | null
|
||||||
|
banReason?: string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
role?: string
|
role?: string
|
||||||
@@ -600,6 +688,9 @@ export type UserUpdateWithoutAccountsInput = {
|
|||||||
emailVerified?: Prisma.BoolFieldUpdateOperationsInput | boolean
|
emailVerified?: Prisma.BoolFieldUpdateOperationsInput | boolean
|
||||||
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
phone?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
phone?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
banned?: Prisma.BoolFieldUpdateOperationsInput | boolean
|
||||||
|
bannedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
|
banReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
role?: Prisma.StringFieldUpdateOperationsInput | string
|
role?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
@@ -614,6 +705,9 @@ export type UserUncheckedUpdateWithoutAccountsInput = {
|
|||||||
emailVerified?: Prisma.BoolFieldUpdateOperationsInput | boolean
|
emailVerified?: Prisma.BoolFieldUpdateOperationsInput | boolean
|
||||||
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
phone?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
phone?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
banned?: Prisma.BoolFieldUpdateOperationsInput | boolean
|
||||||
|
bannedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
|
banReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
role?: Prisma.StringFieldUpdateOperationsInput | string
|
role?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
@@ -628,6 +722,9 @@ export type UserCreateWithoutComplexesInput = {
|
|||||||
emailVerified?: boolean
|
emailVerified?: boolean
|
||||||
image?: string | null
|
image?: string | null
|
||||||
phone?: string | null
|
phone?: string | null
|
||||||
|
banned?: boolean
|
||||||
|
bannedAt?: Date | string | null
|
||||||
|
banReason?: string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
role?: string
|
role?: string
|
||||||
@@ -642,6 +739,9 @@ export type UserUncheckedCreateWithoutComplexesInput = {
|
|||||||
emailVerified?: boolean
|
emailVerified?: boolean
|
||||||
image?: string | null
|
image?: string | null
|
||||||
phone?: string | null
|
phone?: string | null
|
||||||
|
banned?: boolean
|
||||||
|
bannedAt?: Date | string | null
|
||||||
|
banReason?: string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
role?: string
|
role?: string
|
||||||
@@ -672,6 +772,9 @@ export type UserUpdateWithoutComplexesInput = {
|
|||||||
emailVerified?: Prisma.BoolFieldUpdateOperationsInput | boolean
|
emailVerified?: Prisma.BoolFieldUpdateOperationsInput | boolean
|
||||||
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
phone?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
phone?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
banned?: Prisma.BoolFieldUpdateOperationsInput | boolean
|
||||||
|
bannedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
|
banReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
role?: Prisma.StringFieldUpdateOperationsInput | string
|
role?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
@@ -686,6 +789,9 @@ export type UserUncheckedUpdateWithoutComplexesInput = {
|
|||||||
emailVerified?: Prisma.BoolFieldUpdateOperationsInput | boolean
|
emailVerified?: Prisma.BoolFieldUpdateOperationsInput | boolean
|
||||||
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
phone?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
phone?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
banned?: Prisma.BoolFieldUpdateOperationsInput | boolean
|
||||||
|
bannedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
|
banReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
role?: Prisma.StringFieldUpdateOperationsInput | string
|
role?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
@@ -749,6 +855,9 @@ export type UserSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = r
|
|||||||
emailVerified?: boolean
|
emailVerified?: boolean
|
||||||
image?: boolean
|
image?: boolean
|
||||||
phone?: boolean
|
phone?: boolean
|
||||||
|
banned?: boolean
|
||||||
|
bannedAt?: boolean
|
||||||
|
banReason?: boolean
|
||||||
createdAt?: boolean
|
createdAt?: boolean
|
||||||
updatedAt?: boolean
|
updatedAt?: boolean
|
||||||
role?: boolean
|
role?: boolean
|
||||||
@@ -765,6 +874,9 @@ export type UserSelectCreateManyAndReturn<ExtArgs extends runtime.Types.Extensio
|
|||||||
emailVerified?: boolean
|
emailVerified?: boolean
|
||||||
image?: boolean
|
image?: boolean
|
||||||
phone?: boolean
|
phone?: boolean
|
||||||
|
banned?: boolean
|
||||||
|
bannedAt?: boolean
|
||||||
|
banReason?: boolean
|
||||||
createdAt?: boolean
|
createdAt?: boolean
|
||||||
updatedAt?: boolean
|
updatedAt?: boolean
|
||||||
role?: boolean
|
role?: boolean
|
||||||
@@ -777,6 +889,9 @@ export type UserSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensio
|
|||||||
emailVerified?: boolean
|
emailVerified?: boolean
|
||||||
image?: boolean
|
image?: boolean
|
||||||
phone?: boolean
|
phone?: boolean
|
||||||
|
banned?: boolean
|
||||||
|
bannedAt?: boolean
|
||||||
|
banReason?: boolean
|
||||||
createdAt?: boolean
|
createdAt?: boolean
|
||||||
updatedAt?: boolean
|
updatedAt?: boolean
|
||||||
role?: boolean
|
role?: boolean
|
||||||
@@ -789,12 +904,15 @@ export type UserSelectScalar = {
|
|||||||
emailVerified?: boolean
|
emailVerified?: boolean
|
||||||
image?: boolean
|
image?: boolean
|
||||||
phone?: boolean
|
phone?: boolean
|
||||||
|
banned?: boolean
|
||||||
|
bannedAt?: boolean
|
||||||
|
banReason?: boolean
|
||||||
createdAt?: boolean
|
createdAt?: boolean
|
||||||
updatedAt?: boolean
|
updatedAt?: boolean
|
||||||
role?: boolean
|
role?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "name" | "email" | "emailVerified" | "image" | "phone" | "createdAt" | "updatedAt" | "role", ExtArgs["result"]["user"]>
|
export type UserOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "name" | "email" | "emailVerified" | "image" | "phone" | "banned" | "bannedAt" | "banReason" | "createdAt" | "updatedAt" | "role", ExtArgs["result"]["user"]>
|
||||||
export type UserInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
export type UserInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
sessions?: boolean | Prisma.User$sessionsArgs<ExtArgs>
|
sessions?: boolean | Prisma.User$sessionsArgs<ExtArgs>
|
||||||
accounts?: boolean | Prisma.User$accountsArgs<ExtArgs>
|
accounts?: boolean | Prisma.User$accountsArgs<ExtArgs>
|
||||||
@@ -818,6 +936,9 @@ export type $UserPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs =
|
|||||||
emailVerified: boolean
|
emailVerified: boolean
|
||||||
image: string | null
|
image: string | null
|
||||||
phone: string | null
|
phone: string | null
|
||||||
|
banned: boolean
|
||||||
|
bannedAt: Date | null
|
||||||
|
banReason: string | null
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
updatedAt: Date
|
updatedAt: Date
|
||||||
role: string
|
role: string
|
||||||
@@ -1253,6 +1374,9 @@ export interface UserFieldRefs {
|
|||||||
readonly emailVerified: Prisma.FieldRef<"User", 'Boolean'>
|
readonly emailVerified: Prisma.FieldRef<"User", 'Boolean'>
|
||||||
readonly image: Prisma.FieldRef<"User", 'String'>
|
readonly image: Prisma.FieldRef<"User", 'String'>
|
||||||
readonly phone: Prisma.FieldRef<"User", 'String'>
|
readonly phone: Prisma.FieldRef<"User", 'String'>
|
||||||
|
readonly banned: Prisma.FieldRef<"User", 'Boolean'>
|
||||||
|
readonly bannedAt: Prisma.FieldRef<"User", 'DateTime'>
|
||||||
|
readonly banReason: Prisma.FieldRef<"User", 'String'>
|
||||||
readonly createdAt: Prisma.FieldRef<"User", 'DateTime'>
|
readonly createdAt: Prisma.FieldRef<"User", 'DateTime'>
|
||||||
readonly updatedAt: Prisma.FieldRef<"User", 'DateTime'>
|
readonly updatedAt: Prisma.FieldRef<"User", 'DateTime'>
|
||||||
readonly role: Prisma.FieldRef<"User", 'String'>
|
readonly role: Prisma.FieldRef<"User", 'String'>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { wrapLayout } from '@/emails/booking-confirmation';
|
||||||
import { dash } from '@better-auth/infra';
|
import { dash } from '@better-auth/infra';
|
||||||
import { betterAuth } from 'better-auth';
|
import { betterAuth } from 'better-auth';
|
||||||
import { prismaAdapter } from 'better-auth/adapters/prisma';
|
import { prismaAdapter } from 'better-auth/adapters/prisma';
|
||||||
@@ -21,21 +22,103 @@ export const auth = betterAuth({
|
|||||||
},
|
},
|
||||||
user: {
|
user: {
|
||||||
additionalFields: {
|
additionalFields: {
|
||||||
|
role: {
|
||||||
|
type: 'string',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
phone: {
|
phone: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
required: false,
|
required: false,
|
||||||
},
|
},
|
||||||
|
banned: {
|
||||||
|
type: 'boolean',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
bannedAt: {
|
||||||
|
type: 'string',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
banReason: {
|
||||||
|
type: 'string',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
emailVerification: {
|
emailVerification: {
|
||||||
sendOnSignUp: true,
|
sendOnSignUp: true,
|
||||||
autoSignInAfterVerification: true,
|
autoSignInAfterVerification: true,
|
||||||
sendVerificationEmail: async ({ user, url }) => {
|
sendVerificationEmail: async ({ user, url }) => {
|
||||||
|
const verificationUrl = new URL(url);
|
||||||
|
const appUrl = process.env.APP_BASE_URL ?? 'http://localhost:5173';
|
||||||
|
verificationUrl.searchParams.set('callbackURL', appUrl);
|
||||||
|
|
||||||
|
const content = `
|
||||||
|
<tr>
|
||||||
|
<td style="padding:24px 20px 16px;">
|
||||||
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0">
|
||||||
|
<tr>
|
||||||
|
<td valign="top">
|
||||||
|
<table role="presentation" cellpadding="0" cellspacing="0" style="display:inline-block;background-color:#f4f4f5;border-radius:999px;padding:4px 12px;">
|
||||||
|
<tr>
|
||||||
|
<td style="font-size:11px;font-weight:700;letter-spacing:0.14em;color:#71717a;text-transform:uppercase;line-height:1.25rem;">
|
||||||
|
Verificación de email
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<h1 style="margin:16px 0 0;font-size:24px;font-weight:700;color:#111827;letter-spacing:-0.025em;">
|
||||||
|
Verificá tu dirección de correo electrónico
|
||||||
|
</h1>
|
||||||
|
</td>
|
||||||
|
<td valign="top" align="right" style="white-space:nowrap;">
|
||||||
|
<table role="presentation" cellpadding="0" cellspacing="0">
|
||||||
|
<tr>
|
||||||
|
<td valign="middle" style="padding-right:8px;">
|
||||||
|
<img src="${appUrl}/playzer-favicon-512-transparent.png" alt="Playzer" width="32" height="32" style="display:block;" />
|
||||||
|
</td>
|
||||||
|
<td valign="middle">
|
||||||
|
<span style="font-size:18px;font-weight:700;color:#475569;letter-spacing:-0.025em;">Playzer</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
padding:0 20px 16px;
|
||||||
|
<p style="margin:0;font-size:15px;color:#374151;line-height:1.6;">
|
||||||
|
Hacé click en el botón de abajo para verificar tu dirección de correo electrónico y empezar a usar Playzer.
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td style="padding:0 20px 24px;">
|
||||||
|
<table role="presentation" cellpadding="0" cellspacing="0">
|
||||||
|
<tr>
|
||||||
|
<td style="background-color:#059669;border-radius:12px;text-align:center;">
|
||||||
|
<a href="${verificationUrl.toString()}" style="display:block;padding:14px 32px;font-size:15px;font-weight:600;color:#ffffff;text-decoration:none;line-height:1.25rem;">Verificar email</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td style="padding:0 20px 24px;">
|
||||||
|
<p style="margin:0;font-size:13px;color:#9ca3af;text-align:center;line-height:1.5;">
|
||||||
|
Si no creaste una cuenta en Playzer, ignorá este mensaje.
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>`;
|
||||||
|
|
||||||
await sendMail({
|
await sendMail({
|
||||||
to: user.email,
|
to: user.email,
|
||||||
subject: 'Verificá tu email en Playzer',
|
subject: 'Verificá tu email en Playzer',
|
||||||
html: `Hacé click para verificar tu email: <a href="${url}">${url}</a>`,
|
html: wrapLayout(content),
|
||||||
text: `Hacé click para verificar tu email: ${url}`,
|
text: `Hacé click para verificar tu email: ${verificationUrl.toString()}`,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
68
apps/backend/src/lib/geoip.ts
Normal file
68
apps/backend/src/lib/geoip.ts
Normal 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 };
|
||||||
34
apps/backend/src/lib/slot-validator.ts
Normal file
34
apps/backend/src/lib/slot-validator.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
function toMinutes(value: string): number {
|
||||||
|
const [hours, minutes] = value.split(':').map((part) => Number(part));
|
||||||
|
return hours * 60 + minutes;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isSlotInPast(
|
||||||
|
bookingDate: Date,
|
||||||
|
startTime: string,
|
||||||
|
slotDurationMinutes: number,
|
||||||
|
now?: Date
|
||||||
|
): boolean {
|
||||||
|
const currentTime = now ?? new Date();
|
||||||
|
|
||||||
|
const todayStart = new Date(
|
||||||
|
currentTime.getFullYear(),
|
||||||
|
currentTime.getMonth(),
|
||||||
|
currentTime.getDate()
|
||||||
|
);
|
||||||
|
|
||||||
|
const bookingLocalStart = new Date(
|
||||||
|
bookingDate.getUTCFullYear(),
|
||||||
|
bookingDate.getUTCMonth(),
|
||||||
|
bookingDate.getUTCDate()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (bookingLocalStart < todayStart) return true;
|
||||||
|
if (bookingLocalStart > todayStart) return false;
|
||||||
|
|
||||||
|
const currentMinutes = currentTime.getHours() * 60 + currentTime.getMinutes();
|
||||||
|
const slotStartMinutes = toMinutes(startTime);
|
||||||
|
const elapsed = currentMinutes - slotStartMinutes;
|
||||||
|
|
||||||
|
return elapsed > slotDurationMinutes / 2;
|
||||||
|
}
|
||||||
@@ -1,7 +1,20 @@
|
|||||||
import { auth } from '@/lib/auth';
|
import { auth } from '@/lib/auth';
|
||||||
|
import { fetchGeoInfo } from '@/lib/geoip';
|
||||||
|
import { db } from '@/lib/prisma';
|
||||||
import type { AppEnv } from '@/types/hono';
|
import type { AppEnv } from '@/types/hono';
|
||||||
import { createMiddleware } from 'hono/factory';
|
import { createMiddleware } from 'hono/factory';
|
||||||
|
|
||||||
|
type SessionWithGeo = {
|
||||||
|
id: string;
|
||||||
|
ipAddress: string | null;
|
||||||
|
userAgent: string | null;
|
||||||
|
country: string | null;
|
||||||
|
city: string | null;
|
||||||
|
countryCode: string | null;
|
||||||
|
latitude: number | null;
|
||||||
|
longitude: number | null;
|
||||||
|
};
|
||||||
|
|
||||||
export const requireAuth = createMiddleware<AppEnv>(async (c, next) => {
|
export const requireAuth = createMiddleware<AppEnv>(async (c, next) => {
|
||||||
const session = await auth.api.getSession({
|
const session = await auth.api.getSession({
|
||||||
headers: c.req.raw.headers,
|
headers: c.req.raw.headers,
|
||||||
@@ -11,8 +24,45 @@ export const requireAuth = createMiddleware<AppEnv>(async (c, next) => {
|
|||||||
return c.json({ message: 'Unauthorized' }, 401);
|
return c.json({ message: 'Unauthorized' }, 401);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const user = session.user as { banned?: boolean; banReason?: string | null };
|
||||||
|
|
||||||
|
if (user.banned) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
message: 'Tu cuenta ha sido bloqueada.',
|
||||||
|
...(user.banReason ? { reason: user.banReason } : {}),
|
||||||
|
},
|
||||||
|
403
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const s = session.session as unknown as SessionWithGeo;
|
||||||
|
|
||||||
c.set('user', session.user);
|
c.set('user', session.user);
|
||||||
c.set('session', session.session);
|
c.set('session', session.session);
|
||||||
|
|
||||||
|
if (s.ipAddress && !s.country) {
|
||||||
|
resolveSessionGeo(s.id, s.ipAddress);
|
||||||
|
}
|
||||||
|
|
||||||
await next();
|
await next();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
async function resolveSessionGeo(sessionId: string, ipAddress: string) {
|
||||||
|
try {
|
||||||
|
const geo = await fetchGeoInfo(ipAddress);
|
||||||
|
if (!geo) return;
|
||||||
|
await db.session.update({
|
||||||
|
where: { id: sessionId },
|
||||||
|
data: {
|
||||||
|
country: geo.country,
|
||||||
|
city: geo.city,
|
||||||
|
countryCode: geo.countryCode,
|
||||||
|
latitude: geo.lat,
|
||||||
|
longitude: geo.lon,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// Silently fail — geo enrichment is best-effort
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,13 +1,19 @@
|
|||||||
import { requireAuth } from '@/middlewares/require-auth.middleware';
|
import { requireAuth } from '@/middlewares/require-auth.middleware';
|
||||||
|
import { cancelRecurringGroupHandler } from '@/modules/admin-booking/handlers/cancel-recurring-group.handler';
|
||||||
import { createAdminBookingHandler } from '@/modules/admin-booking/handlers/create-admin-booking.handler';
|
import { createAdminBookingHandler } from '@/modules/admin-booking/handlers/create-admin-booking.handler';
|
||||||
|
import { createAdminRecurringBookingHandler } from '@/modules/admin-booking/handlers/create-admin-recurring-booking.handler';
|
||||||
import { listAdminBookingsHandler } from '@/modules/admin-booking/handlers/list-admin-bookings.handler';
|
import { listAdminBookingsHandler } from '@/modules/admin-booking/handlers/list-admin-bookings.handler';
|
||||||
|
import { listRecurringGroupsHandler } from '@/modules/admin-booking/handlers/list-recurring-groups.handler';
|
||||||
import { updateAdminBookingStatusHandler } from '@/modules/admin-booking/handlers/update-admin-booking-status.handler';
|
import { updateAdminBookingStatusHandler } from '@/modules/admin-booking/handlers/update-admin-booking-status.handler';
|
||||||
|
import { updateRecurringGroupHandler } from '@/modules/admin-booking/handlers/update-recurring-group.handler';
|
||||||
import type { AppEnv } from '@/types/hono';
|
import type { AppEnv } from '@/types/hono';
|
||||||
import { zValidator } from '@hono/zod-validator';
|
import { zValidator } from '@hono/zod-validator';
|
||||||
import {
|
import {
|
||||||
createAdminBookingSchema,
|
createAdminBookingSchema,
|
||||||
|
createRecurringBookingSchema,
|
||||||
listAdminBookingsQuerySchema,
|
listAdminBookingsQuerySchema,
|
||||||
updateAdminBookingStatusSchema,
|
updateAdminBookingStatusSchema,
|
||||||
|
updateRecurringGroupSchema,
|
||||||
} from '@repo/api-contract';
|
} from '@repo/api-contract';
|
||||||
import { Hono } from 'hono';
|
import { Hono } from 'hono';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
@@ -15,6 +21,7 @@ import { z } from 'zod';
|
|||||||
export const adminBookingRoutes = new Hono<AppEnv>();
|
export const adminBookingRoutes = new Hono<AppEnv>();
|
||||||
const complexIdParamsSchema = z.object({ complexId: z.uuid() });
|
const complexIdParamsSchema = z.object({ complexId: z.uuid() });
|
||||||
const bookingIdParamsSchema = z.object({ id: z.uuid() });
|
const bookingIdParamsSchema = z.object({ id: z.uuid() });
|
||||||
|
const groupIdParamsSchema = z.object({ groupId: z.uuid() });
|
||||||
|
|
||||||
adminBookingRoutes.use('*', requireAuth);
|
adminBookingRoutes.use('*', requireAuth);
|
||||||
|
|
||||||
@@ -32,6 +39,32 @@ adminBookingRoutes.post(
|
|||||||
createAdminBookingHandler
|
createAdminBookingHandler
|
||||||
);
|
);
|
||||||
|
|
||||||
|
adminBookingRoutes.post(
|
||||||
|
'/complex/:complexId/recurring',
|
||||||
|
zValidator('param', complexIdParamsSchema),
|
||||||
|
zValidator('json', createRecurringBookingSchema),
|
||||||
|
createAdminRecurringBookingHandler
|
||||||
|
);
|
||||||
|
|
||||||
|
adminBookingRoutes.post(
|
||||||
|
'/recurring/:groupId/cancel',
|
||||||
|
zValidator('param', groupIdParamsSchema),
|
||||||
|
cancelRecurringGroupHandler
|
||||||
|
);
|
||||||
|
|
||||||
|
adminBookingRoutes.get(
|
||||||
|
'/complex/:complexId/recurring',
|
||||||
|
zValidator('param', complexIdParamsSchema),
|
||||||
|
listRecurringGroupsHandler
|
||||||
|
);
|
||||||
|
|
||||||
|
adminBookingRoutes.patch(
|
||||||
|
'/recurring/:groupId',
|
||||||
|
zValidator('param', groupIdParamsSchema),
|
||||||
|
zValidator('json', updateRecurringGroupSchema),
|
||||||
|
updateRecurringGroupHandler
|
||||||
|
);
|
||||||
|
|
||||||
adminBookingRoutes.patch(
|
adminBookingRoutes.patch(
|
||||||
'/:id/status',
|
'/:id/status',
|
||||||
zValidator('param', bookingIdParamsSchema),
|
zValidator('param', bookingIdParamsSchema),
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
|
import { db } from '@/lib/prisma';
|
||||||
import {
|
import {
|
||||||
AdminBookingServiceError,
|
AdminBookingServiceError,
|
||||||
createAdminBooking,
|
createAdminBooking,
|
||||||
} from '@/modules/admin-booking/services/admin-booking.service';
|
} from '@/modules/admin-booking/services/admin-booking.service';
|
||||||
|
import { sendBookingConfirmation } from '@/services/booking-email.service';
|
||||||
import type { AppContext } from '@/types/hono';
|
import type { AppContext } from '@/types/hono';
|
||||||
import type { CreateAdminBookingInput } from '@repo/api-contract';
|
import type { CreateAdminBookingInput } from '@repo/api-contract';
|
||||||
|
|
||||||
@@ -14,6 +16,26 @@ export async function createAdminBookingHandler(c: AppContext) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const booking = await createAdminBooking(user.id, complexId, payload);
|
const booking = await createAdminBooking(user.id, complexId, payload);
|
||||||
|
|
||||||
|
const complex = await db.complex.findUnique({
|
||||||
|
where: { id: complexId },
|
||||||
|
select: { complexSlug: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
void sendBookingConfirmation({
|
||||||
|
bookingCode: booking.bookingCode,
|
||||||
|
complexSlug: complex?.complexSlug ?? '',
|
||||||
|
complexName: booking.complexName,
|
||||||
|
date: booking.date,
|
||||||
|
startTime: booking.startTime,
|
||||||
|
endTime: booking.endTime,
|
||||||
|
courtName: booking.courtName,
|
||||||
|
sportName: booking.sport.name,
|
||||||
|
customerName: booking.customerName,
|
||||||
|
customerEmail: booking.customerEmail,
|
||||||
|
price: booking.price,
|
||||||
|
});
|
||||||
|
|
||||||
return c.json(booking, 201);
|
return c.json(booking, 201);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof AdminBookingServiceError) {
|
if (error instanceof AdminBookingServiceError) {
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { AdminBookingServiceError } from '@/modules/admin-booking/services/admin-booking.service';
|
||||||
|
import { createAdminRecurringBooking } from '@/modules/admin-booking/services/admin-recurring-booking.service';
|
||||||
|
import { sendBookingConfirmation } from '@/services/booking-email.service';
|
||||||
|
import type { AppContext } from '@/types/hono';
|
||||||
|
import type { CreateRecurringBookingInput } from '@repo/api-contract';
|
||||||
|
|
||||||
|
type ComplexIdParams = { complexId: string };
|
||||||
|
|
||||||
|
export async function createAdminRecurringBookingHandler(c: AppContext) {
|
||||||
|
const { complexId } = c.req.valid('param' as never) as ComplexIdParams;
|
||||||
|
const payload = c.req.valid('json' as never) as CreateRecurringBookingInput;
|
||||||
|
const user = c.get('user');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const group = await createAdminRecurringBooking(user.id, complexId, payload);
|
||||||
|
|
||||||
|
const firstBooking = group.bookings[0];
|
||||||
|
if (firstBooking?.customerEmail) {
|
||||||
|
void sendBookingConfirmation({
|
||||||
|
bookingCode: firstBooking.bookingCode,
|
||||||
|
complexName: firstBooking.complexName,
|
||||||
|
date: firstBooking.date,
|
||||||
|
startTime: firstBooking.startTime,
|
||||||
|
endTime: firstBooking.endTime,
|
||||||
|
courtName: firstBooking.courtName,
|
||||||
|
sportName: firstBooking.sport.name,
|
||||||
|
customerName: firstBooking.customerName,
|
||||||
|
customerEmail: firstBooking.customerEmail,
|
||||||
|
price: firstBooking.price,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.json(group, 201);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof AdminBookingServiceError) {
|
||||||
|
return c.json({ message: error.message }, error.status);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { AdminBookingServiceError } from '@/modules/admin-booking/services/admin-booking.service';
|
||||||
|
import { listRecurringGroups } from '@/modules/admin-booking/services/admin-recurring-booking.service';
|
||||||
|
import type { AppContext } from '@/types/hono';
|
||||||
|
|
||||||
|
type ComplexIdParams = { complexId: string };
|
||||||
|
|
||||||
|
export async function listRecurringGroupsHandler(c: AppContext) {
|
||||||
|
const { complexId } = c.req.valid('param' as never) as ComplexIdParams;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const groups = await listRecurringGroups(complexId);
|
||||||
|
return c.json({ groups });
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof AdminBookingServiceError) {
|
||||||
|
return c.json({ message: error.message }, error.status);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import {
|
|||||||
AdminBookingServiceError,
|
AdminBookingServiceError,
|
||||||
updateAdminBookingStatus,
|
updateAdminBookingStatus,
|
||||||
} from '@/modules/admin-booking/services/admin-booking.service';
|
} from '@/modules/admin-booking/services/admin-booking.service';
|
||||||
|
import { sendBookingCancelled, sendBookingNoShow } from '@/services/booking-email.service';
|
||||||
import type { AppContext } from '@/types/hono';
|
import type { AppContext } from '@/types/hono';
|
||||||
import type { UpdateAdminBookingStatusInput } from '@repo/api-contract';
|
import type { UpdateAdminBookingStatusInput } from '@repo/api-contract';
|
||||||
|
|
||||||
@@ -14,6 +15,33 @@ export async function updateAdminBookingStatusHandler(c: AppContext) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const booking = await updateAdminBookingStatus(user.id, id, payload);
|
const booking = await updateAdminBookingStatus(user.id, id, payload);
|
||||||
|
|
||||||
|
if (payload.status === 'CANCELLED') {
|
||||||
|
void sendBookingCancelled({
|
||||||
|
bookingCode: booking.bookingCode,
|
||||||
|
complexName: booking.complexName,
|
||||||
|
date: booking.date,
|
||||||
|
startTime: booking.startTime,
|
||||||
|
endTime: booking.endTime,
|
||||||
|
courtName: booking.courtName,
|
||||||
|
sportName: booking.sport.name,
|
||||||
|
customerName: booking.customerName,
|
||||||
|
customerEmail: booking.customerEmail,
|
||||||
|
});
|
||||||
|
} else if (payload.status === 'NOSHOW') {
|
||||||
|
void sendBookingNoShow({
|
||||||
|
bookingCode: booking.bookingCode,
|
||||||
|
complexName: booking.complexName,
|
||||||
|
date: booking.date,
|
||||||
|
startTime: booking.startTime,
|
||||||
|
endTime: booking.endTime,
|
||||||
|
courtName: booking.courtName,
|
||||||
|
sportName: booking.sport.name,
|
||||||
|
customerName: booking.customerName,
|
||||||
|
customerEmail: booking.customerEmail,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return c.json(booking);
|
return c.json(booking);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof AdminBookingServiceError) {
|
if (error instanceof AdminBookingServiceError) {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
import { randomInt } from 'node:crypto';
|
import { randomInt } from 'node:crypto';
|
||||||
import { CourtBookingStatus } from '@/generated/prisma/enums';
|
import { CourtBookingStatus } from '@/generated/prisma/enums';
|
||||||
import { db } from '@/lib/prisma';
|
import { db } from '@/lib/prisma';
|
||||||
|
import { isSlotInPast } from '@/lib/slot-validator';
|
||||||
import { evaluatePlanUsage, parsePlanRules } from '@/modules/plan/services/plan-rules.service';
|
import { evaluatePlanUsage, parsePlanRules } from '@/modules/plan/services/plan-rules.service';
|
||||||
|
import type { DayOfWeek } from '@repo/api-contract';
|
||||||
import type {
|
import type {
|
||||||
AdminBooking,
|
AdminBooking,
|
||||||
CreateAdminBookingInput,
|
CreateAdminBookingInput,
|
||||||
@@ -156,6 +158,40 @@ async function ensureComplexAccess(complexId: string, userId: string) {
|
|||||||
return complexUser.complex;
|
return complexUser.complex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolvePrice(
|
||||||
|
court: {
|
||||||
|
basePrice: unknown;
|
||||||
|
priceRules: Array<{
|
||||||
|
dayOfWeek: string | null;
|
||||||
|
startTime: string | null;
|
||||||
|
endTime: string | null;
|
||||||
|
price: unknown;
|
||||||
|
}>;
|
||||||
|
},
|
||||||
|
dayOfWeek: string,
|
||||||
|
startTime: string,
|
||||||
|
endTime: string
|
||||||
|
): number {
|
||||||
|
const slotStart = toMinutes(startTime);
|
||||||
|
const slotEnd = toMinutes(endTime);
|
||||||
|
|
||||||
|
const matchingRules = court.priceRules
|
||||||
|
.filter((rule) => {
|
||||||
|
if (rule.dayOfWeek && rule.dayOfWeek !== dayOfWeek) return false;
|
||||||
|
if (!rule.startTime || !rule.endTime) return true;
|
||||||
|
return slotStart >= toMinutes(rule.startTime) && slotEnd <= toMinutes(rule.endTime);
|
||||||
|
})
|
||||||
|
.sort((first, second) => {
|
||||||
|
const firstSpecificity =
|
||||||
|
(first.dayOfWeek ? 2 : 0) + (first.startTime && first.endTime ? 1 : 0);
|
||||||
|
const secondSpecificity =
|
||||||
|
(second.dayOfWeek ? 2 : 0) + (second.startTime && second.endTime ? 1 : 0);
|
||||||
|
return secondSpecificity - firstSpecificity;
|
||||||
|
});
|
||||||
|
|
||||||
|
return Number(matchingRules[0]?.price ?? court.basePrice);
|
||||||
|
}
|
||||||
|
|
||||||
function mapBookingResponse(booking: {
|
function mapBookingResponse(booking: {
|
||||||
id: string;
|
id: string;
|
||||||
bookingCode: string;
|
bookingCode: string;
|
||||||
@@ -164,7 +200,9 @@ function mapBookingResponse(booking: {
|
|||||||
endTime: string;
|
endTime: string;
|
||||||
customerName: string;
|
customerName: string;
|
||||||
customerPhone: string;
|
customerPhone: string;
|
||||||
|
customerEmail: string;
|
||||||
status: 'CONFIRMED' | 'CANCELLED' | 'COMPLETED' | 'NOSHOW';
|
status: 'CONFIRMED' | 'CANCELLED' | 'COMPLETED' | 'NOSHOW';
|
||||||
|
recurringGroupId: string | null;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
court: {
|
court: {
|
||||||
@@ -180,6 +218,7 @@ function mapBookingResponse(booking: {
|
|||||||
slug: string;
|
slug: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
price?: number;
|
||||||
}): AdminBooking {
|
}): AdminBooking {
|
||||||
return {
|
return {
|
||||||
id: booking.id,
|
id: booking.id,
|
||||||
@@ -198,7 +237,10 @@ function mapBookingResponse(booking: {
|
|||||||
endTime: booking.endTime,
|
endTime: booking.endTime,
|
||||||
customerName: booking.customerName,
|
customerName: booking.customerName,
|
||||||
customerPhone: booking.customerPhone,
|
customerPhone: booking.customerPhone,
|
||||||
|
customerEmail: booking.customerEmail,
|
||||||
|
price: booking.price ?? 0,
|
||||||
status: booking.status,
|
status: booking.status,
|
||||||
|
recurringGroupId: booking.recurringGroupId,
|
||||||
createdAt: booking.createdAt.toISOString(),
|
createdAt: booking.createdAt.toISOString(),
|
||||||
updatedAt: booking.updatedAt.toISOString(),
|
updatedAt: booking.updatedAt.toISOString(),
|
||||||
};
|
};
|
||||||
@@ -258,6 +300,16 @@ export async function createAdminBooking(
|
|||||||
input: CreateAdminBookingInput
|
input: CreateAdminBookingInput
|
||||||
) {
|
) {
|
||||||
const complex = await ensureComplexAccess(complexId, userId);
|
const complex = await ensureComplexAccess(complexId, userId);
|
||||||
|
|
||||||
|
const adminUser = await db.user.findUnique({
|
||||||
|
where: { id: userId },
|
||||||
|
select: { emailVerified: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!adminUser?.emailVerified) {
|
||||||
|
throw new AdminBookingServiceError('Debés verificar tu email para poder crear reservas.', 403);
|
||||||
|
}
|
||||||
|
|
||||||
const bookingDate = parseIsoDate(input.date);
|
const bookingDate = parseIsoDate(input.date);
|
||||||
const dayOfWeek = getDayOfWeek(bookingDate);
|
const dayOfWeek = getDayOfWeek(bookingDate);
|
||||||
|
|
||||||
@@ -282,6 +334,10 @@ export async function createAdminBooking(
|
|||||||
slug: true,
|
slug: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
priceRules: {
|
||||||
|
where: { isActive: true },
|
||||||
|
orderBy: [{ dayOfWeek: 'asc' }, { startTime: 'asc' }],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -308,6 +364,10 @@ export async function createAdminBooking(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isSlotInPast(bookingDate, input.startTime, court.slotDurationMinutes)) {
|
||||||
|
throw new AdminBookingServiceError('No se pueden crear reservas en el pasado.', 400);
|
||||||
|
}
|
||||||
|
|
||||||
for (let attempt = 0; attempt < 5; attempt += 1) {
|
for (let attempt = 0; attempt < 5; attempt += 1) {
|
||||||
try {
|
try {
|
||||||
const booking = await db.$transaction(async (tx) => {
|
const booking = await db.$transaction(async (tx) => {
|
||||||
@@ -371,7 +431,9 @@ export async function createAdminBooking(
|
|||||||
endTime: selectedSlot.endTime,
|
endTime: selectedSlot.endTime,
|
||||||
customerName: input.customerName.trim(),
|
customerName: input.customerName.trim(),
|
||||||
customerPhone: input.customerPhone.trim(),
|
customerPhone: input.customerPhone.trim(),
|
||||||
|
customerEmail: input.customerEmail?.trim() ?? '',
|
||||||
status: 'CONFIRMED',
|
status: 'CONFIRMED',
|
||||||
|
recurringGroupId: null,
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
court: {
|
court: {
|
||||||
@@ -397,7 +459,9 @@ export async function createAdminBooking(
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return mapBookingResponse(booking);
|
const price = resolvePrice(court, dayOfWeek, selectedSlot.startTime, selectedSlot.endTime);
|
||||||
|
|
||||||
|
return mapBookingResponse({ ...booking, price });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof AdminBookingServiceError) {
|
if (error instanceof AdminBookingServiceError) {
|
||||||
throw error;
|
throw error;
|
||||||
@@ -511,6 +575,7 @@ export async function updateAdminBookingStatus(
|
|||||||
endTime: booking.endTime,
|
endTime: booking.endTime,
|
||||||
customerName: booking.customerName,
|
customerName: booking.customerName,
|
||||||
customerPhone: booking.customerPhone,
|
customerPhone: booking.customerPhone,
|
||||||
|
customerEmail: booking.customerEmail,
|
||||||
previousStatus: booking.status,
|
previousStatus: booking.status,
|
||||||
newStatus: input.status,
|
newStatus: input.status,
|
||||||
changedAt: new Date(),
|
changedAt: new Date(),
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
};
|
||||||
|
}
|
||||||
67
apps/backend/src/modules/admin/admin.routes.ts
Normal file
67
apps/backend/src/modules/admin/admin.routes.ts
Normal 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
|
||||||
|
);
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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.' });
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
};
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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.` });
|
||||||
|
}
|
||||||
@@ -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.' });
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
});
|
||||||
|
}
|
||||||
67
apps/backend/src/modules/admin/services/admin-geo.service.ts
Normal file
67
apps/backend/src/modules/admin/services/admin-geo.service.ts
Normal 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 };
|
||||||
|
}
|
||||||
146
apps/backend/src/modules/admin/services/admin-users.service.ts
Normal file
146
apps/backend/src/modules/admin/services/admin-users.service.ts
Normal 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;
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Prisma } from '@/generated/prisma/client';
|
import { Prisma } from '@/generated/prisma/client';
|
||||||
import { db } from '@/lib/prisma';
|
import { db } from '@/lib/prisma';
|
||||||
|
import { parsePlanRules } from '@/modules/plan/services/plan-rules.service';
|
||||||
import { v7 as uuidv7 } from 'uuid';
|
import { v7 as uuidv7 } from 'uuid';
|
||||||
|
|
||||||
export type CreateComplexInput = {
|
export type CreateComplexInput = {
|
||||||
@@ -185,7 +186,11 @@ export async function listMyComplexes(userId: string) {
|
|||||||
const complexUsers = await db.complexUser.findMany({
|
const complexUsers = await db.complexUser.findMany({
|
||||||
where: { userId },
|
where: { userId },
|
||||||
include: {
|
include: {
|
||||||
complex: true,
|
complex: {
|
||||||
|
include: {
|
||||||
|
plan: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
orderBy: {
|
orderBy: {
|
||||||
createdAt: 'asc',
|
createdAt: 'asc',
|
||||||
@@ -195,6 +200,9 @@ export async function listMyComplexes(userId: string) {
|
|||||||
return complexUsers.map((complexUser) => ({
|
return complexUsers.map((complexUser) => ({
|
||||||
...complexUser.complex,
|
...complexUser.complex,
|
||||||
role: complexUser.role,
|
role: complexUser.role,
|
||||||
|
planFeatures: complexUser.complex.plan
|
||||||
|
? parsePlanRules(complexUser.complex.plan.rules).features
|
||||||
|
: null,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,7 +215,11 @@ export async function getCurrentComplex(userId: string, complexId: string) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
complex: true,
|
complex: {
|
||||||
|
include: {
|
||||||
|
plan: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -216,6 +228,9 @@ export async function getCurrentComplex(userId: string, complexId: string) {
|
|||||||
return {
|
return {
|
||||||
...complexUser.complex,
|
...complexUser.complex,
|
||||||
role: complexUser.role,
|
role: complexUser.role,
|
||||||
|
planFeatures: complexUser.complex.plan
|
||||||
|
? parsePlanRules(complexUser.complex.plan.rules).features
|
||||||
|
: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,7 +243,11 @@ export async function selectComplex(userId: string, complexId: string) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
complex: true,
|
complex: {
|
||||||
|
include: {
|
||||||
|
plan: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -237,6 +256,9 @@ export async function selectComplex(userId: string, complexId: string) {
|
|||||||
return {
|
return {
|
||||||
...complexUser.complex,
|
...complexUser.complex,
|
||||||
role: complexUser.role,
|
role: complexUser.role,
|
||||||
|
planFeatures: complexUser.complex.plan
|
||||||
|
? parsePlanRules(complexUser.complex.plan.rules).features
|
||||||
|
: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { createHash, randomInt } from 'node:crypto';
|
import { createHash, randomInt } from 'node:crypto';
|
||||||
import { auth } from '@/lib/auth';
|
import { auth } from '@/lib/auth';
|
||||||
|
import { type IpGeoInfo, fetchGeoInfo } from '@/lib/geoip';
|
||||||
import { sendMail } from '@/lib/mailer';
|
import { sendMail } from '@/lib/mailer';
|
||||||
import { db } from '@/lib/prisma';
|
import { db } from '@/lib/prisma';
|
||||||
import type {
|
import type {
|
||||||
@@ -82,13 +83,6 @@ type UserAgentInfo = {
|
|||||||
device: string;
|
device: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type IpGeoInfo = {
|
|
||||||
ip: string;
|
|
||||||
city: string;
|
|
||||||
country: string;
|
|
||||||
countryCode: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
function parseUserAgent(ua: string | undefined): UserAgentInfo {
|
function parseUserAgent(ua: string | undefined): UserAgentInfo {
|
||||||
if (!ua) {
|
if (!ua) {
|
||||||
return { browser: 'Desconocido', os: 'Desconocido', device: 'Desconocido' };
|
return { browser: 'Desconocido', os: 'Desconocido', device: 'Desconocido' };
|
||||||
@@ -118,31 +112,6 @@ function parseUserAgent(ua: string | undefined): UserAgentInfo {
|
|||||||
return { browser, os, device };
|
return { browser, os, device };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchGeoInfo(ip: string): Promise<IpGeoInfo | null> {
|
|
||||||
if (ip === '127.0.0.1' || ip === '::1' || ip.startsWith('192.168.') || ip.startsWith('10.')) {
|
|
||||||
return { ip, city: 'Red local', country: 'Red local', countryCode: 'LOCAL' };
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(
|
|
||||||
`http://ip-api.com/json/${ip}?fields=status,country,countryCode,city,query`
|
|
||||||
);
|
|
||||||
if (!response.ok) return null;
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
if (data.status !== 'success') return null;
|
|
||||||
|
|
||||||
return {
|
|
||||||
ip: data.query,
|
|
||||||
city: data.city || 'Desconocida',
|
|
||||||
country: data.country || 'Desconocido',
|
|
||||||
countryCode: data.countryCode || '',
|
|
||||||
};
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function sendResetOtpEmail(email: string, otpCode: string) {
|
async function sendResetOtpEmail(email: string, otpCode: string) {
|
||||||
await sendMail({
|
await sendMail({
|
||||||
to: email,
|
to: email,
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
import { fetchGeoInfo } from '@/lib/geoip';
|
||||||
import { db } from '@/lib/prisma';
|
import { db } from '@/lib/prisma';
|
||||||
|
import { resolvePlanPrice } from '@/modules/plan/services/plan-pricing.service';
|
||||||
import { parsePlanRules } from '@/modules/plan/services/plan-rules.service';
|
import { parsePlanRules } from '@/modules/plan/services/plan-rules.service';
|
||||||
import type { AppContext } from '@/types/hono';
|
import type { AppContext } from '@/types/hono';
|
||||||
|
|
||||||
@@ -9,13 +11,26 @@ export async function listPlansHandler(c: AppContext) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const countryParam = c.req.query('country');
|
||||||
|
let countryCode: string | undefined;
|
||||||
|
|
||||||
|
if (countryParam) {
|
||||||
|
countryCode = countryParam;
|
||||||
|
} else {
|
||||||
|
const ip = c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || '127.0.0.1';
|
||||||
|
const geo = await fetchGeoInfo(ip);
|
||||||
|
countryCode = geo?.countryCode || undefined;
|
||||||
|
}
|
||||||
|
|
||||||
return c.json(
|
return c.json(
|
||||||
plans.map((plan) => {
|
plans.map((plan) => {
|
||||||
const rules = parsePlanRules(plan.rules);
|
const rules = parsePlanRules(plan.rules);
|
||||||
|
const resolved = resolvePlanPrice(Number(plan.price), rules, countryCode);
|
||||||
return {
|
return {
|
||||||
code: plan.code,
|
code: plan.code,
|
||||||
name: plan.name,
|
name: plan.name,
|
||||||
price: Number(plan.price),
|
price: resolved.amount,
|
||||||
|
currency: resolved.currency,
|
||||||
features: rules.features,
|
features: rules.features,
|
||||||
limits: rules.limits,
|
limits: rules.limits,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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' };
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { sseManager } from '@/lib/sse';
|
||||||
|
import {
|
||||||
|
PublicBookingServiceError,
|
||||||
|
cancelPublicBooking,
|
||||||
|
} from '@/modules/public-booking/services/public-booking.service';
|
||||||
|
import { sendBookingCancelled } from '@/services/booking-email.service';
|
||||||
|
import type { AppContext } from '@/types/hono';
|
||||||
|
import type { CancelPublicBookingInput } from '@repo/api-contract';
|
||||||
|
|
||||||
|
type ComplexSlugParams = { complexSlug: string };
|
||||||
|
|
||||||
|
export async function cancelPublicBookingHandler(c: AppContext) {
|
||||||
|
const { complexSlug } = c.req.valid('param' as never) as ComplexSlugParams;
|
||||||
|
const payload = c.req.valid('json' as never) as CancelPublicBookingInput;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const booking = await cancelPublicBooking(complexSlug, payload);
|
||||||
|
|
||||||
|
const channel = `complex-${booking.complexId}`;
|
||||||
|
sseManager.emit(
|
||||||
|
channel,
|
||||||
|
JSON.stringify({
|
||||||
|
type: 'booking_cancelled',
|
||||||
|
booking: {
|
||||||
|
bookingCode: booking.bookingCode,
|
||||||
|
courtId: booking.courtId,
|
||||||
|
date: booking.date,
|
||||||
|
startTime: booking.startTime,
|
||||||
|
endTime: booking.endTime,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
void sendBookingCancelled({
|
||||||
|
bookingCode: booking.bookingCode,
|
||||||
|
complexSlug: booking.complexSlug,
|
||||||
|
complexName: booking.complexName,
|
||||||
|
date: booking.date,
|
||||||
|
startTime: booking.startTime,
|
||||||
|
endTime: booking.endTime,
|
||||||
|
courtName: booking.courtName,
|
||||||
|
sportName: booking.sport.name,
|
||||||
|
customerName: booking.customerName,
|
||||||
|
customerEmail: booking.customerEmail,
|
||||||
|
});
|
||||||
|
|
||||||
|
return c.json(booking);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof PublicBookingServiceError) {
|
||||||
|
return c.json({ message: error.message }, error.status);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
PublicBookingServiceError,
|
PublicBookingServiceError,
|
||||||
createPublicBooking,
|
createPublicBooking,
|
||||||
} from '@/modules/public-booking/services/public-booking.service';
|
} from '@/modules/public-booking/services/public-booking.service';
|
||||||
|
import { sendBookingConfirmation } from '@/services/booking-email.service';
|
||||||
import type { AppContext } from '@/types/hono';
|
import type { AppContext } from '@/types/hono';
|
||||||
import type { CreatePublicBookingInput } from '@repo/api-contract';
|
import type { CreatePublicBookingInput } from '@repo/api-contract';
|
||||||
|
|
||||||
@@ -30,6 +31,20 @@ export async function createPublicBookingHandler(c: AppContext) {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
void sendBookingConfirmation({
|
||||||
|
bookingCode: booking.bookingCode,
|
||||||
|
complexSlug: booking.complexSlug,
|
||||||
|
complexName: booking.complexName,
|
||||||
|
date: booking.date,
|
||||||
|
startTime: booking.startTime,
|
||||||
|
endTime: booking.endTime,
|
||||||
|
courtName: booking.courtName,
|
||||||
|
sportName: booking.sport.name,
|
||||||
|
customerName: booking.customerName,
|
||||||
|
customerEmail: booking.customerEmail,
|
||||||
|
price: booking.price,
|
||||||
|
});
|
||||||
|
|
||||||
return c.json(booking, 201);
|
return c.json(booking, 201);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof PublicBookingServiceError) {
|
if (error instanceof PublicBookingServiceError) {
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
|
import { cancelPublicBookingHandler } from '@/modules/public-booking/handlers/cancel-public-booking.handler';
|
||||||
import { createPublicBookingHandler } from '@/modules/public-booking/handlers/create-public-booking.handler';
|
import { createPublicBookingHandler } from '@/modules/public-booking/handlers/create-public-booking.handler';
|
||||||
import { getPublicBookingConfirmationHandler } from '@/modules/public-booking/handlers/get-public-booking-confirmation.handler';
|
import { getPublicBookingConfirmationHandler } from '@/modules/public-booking/handlers/get-public-booking-confirmation.handler';
|
||||||
import { listPublicAvailabilityHandler } from '@/modules/public-booking/handlers/list-public-availability.handler';
|
import { listPublicAvailabilityHandler } from '@/modules/public-booking/handlers/list-public-availability.handler';
|
||||||
import type { AppEnv } from '@/types/hono';
|
import type { AppEnv } from '@/types/hono';
|
||||||
import { zValidator } from '@hono/zod-validator';
|
import { zValidator } from '@hono/zod-validator';
|
||||||
import { createPublicBookingSchema, publicAvailabilityQuerySchema } from '@repo/api-contract';
|
import {
|
||||||
|
cancelPublicBookingSchema,
|
||||||
|
createPublicBookingSchema,
|
||||||
|
publicAvailabilityQuerySchema,
|
||||||
|
} from '@repo/api-contract';
|
||||||
import { Hono } from 'hono';
|
import { Hono } from 'hono';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
@@ -33,3 +38,10 @@ publicBookingRoutes.get(
|
|||||||
zValidator('param', confirmationParamsSchema),
|
zValidator('param', confirmationParamsSchema),
|
||||||
getPublicBookingConfirmationHandler
|
getPublicBookingConfirmationHandler
|
||||||
);
|
);
|
||||||
|
|
||||||
|
publicBookingRoutes.post(
|
||||||
|
'/complex/:complexSlug/cancel',
|
||||||
|
zValidator('param', complexSlugParamsSchema),
|
||||||
|
zValidator('json', cancelPublicBookingSchema),
|
||||||
|
cancelPublicBookingHandler
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { randomInt } from 'node:crypto';
|
import { randomInt } from 'node:crypto';
|
||||||
import { db } from '@/lib/prisma';
|
import { db } from '@/lib/prisma';
|
||||||
|
import { isSlotInPast } from '@/lib/slot-validator';
|
||||||
import { evaluatePlanUsage, parsePlanRules } from '@/modules/plan/services/plan-rules.service';
|
import { evaluatePlanUsage, parsePlanRules } from '@/modules/plan/services/plan-rules.service';
|
||||||
import type {
|
import type {
|
||||||
|
CancelPublicBookingInput,
|
||||||
CreatePublicBookingInput,
|
CreatePublicBookingInput,
|
||||||
DayOfWeek,
|
DayOfWeek,
|
||||||
PublicAvailabilityQuery,
|
PublicAvailabilityQuery,
|
||||||
@@ -13,6 +15,16 @@ type Slot = {
|
|||||||
endTime: string;
|
endTime: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type PriceableCourt = {
|
||||||
|
basePrice: unknown;
|
||||||
|
priceRules: Array<{
|
||||||
|
dayOfWeek: DayOfWeek | null;
|
||||||
|
startTime: string | null;
|
||||||
|
endTime: string | null;
|
||||||
|
price: unknown;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
|
||||||
type ComplexWithPublicBookingData = Awaited<ReturnType<typeof getComplexWithBookingData>>;
|
type ComplexWithPublicBookingData = Awaited<ReturnType<typeof getComplexWithBookingData>>;
|
||||||
|
|
||||||
const DAY_OF_WEEK_BY_INDEX: DayOfWeek[] = [
|
const DAY_OF_WEEK_BY_INDEX: DayOfWeek[] = [
|
||||||
@@ -32,7 +44,6 @@ export class PublicBookingServiceError extends Error {
|
|||||||
|
|
||||||
constructor(message: string, status: 400 | 403 | 404 | 409) {
|
constructor(message: string, status: 400 | 403 | 404 | 409) {
|
||||||
super(message);
|
super(message);
|
||||||
this.name = 'PublicBookingServiceError';
|
|
||||||
this.status = status;
|
this.status = status;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -202,6 +213,10 @@ async function getComplexWithBookingData(complexSlug: string) {
|
|||||||
availabilities: {
|
availabilities: {
|
||||||
orderBy: [{ dayOfWeek: 'asc' }, { startTime: 'asc' }],
|
orderBy: [{ dayOfWeek: 'asc' }, { startTime: 'asc' }],
|
||||||
},
|
},
|
||||||
|
priceRules: {
|
||||||
|
where: { isActive: true },
|
||||||
|
orderBy: [{ dayOfWeek: 'asc' }, { startTime: 'asc' }],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
orderBy: { createdAt: 'asc' },
|
orderBy: { createdAt: 'asc' },
|
||||||
},
|
},
|
||||||
@@ -226,6 +241,33 @@ async function getComplexWithBookingData(complexSlug: string) {
|
|||||||
return complex;
|
return complex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveSlotPrice(court: PriceableCourt, dayOfWeek: DayOfWeek, slot: Slot): number {
|
||||||
|
const slotStart = toMinutes(slot.startTime);
|
||||||
|
const slotEnd = toMinutes(slot.endTime);
|
||||||
|
const matchingRules = court.priceRules
|
||||||
|
.filter((rule) => {
|
||||||
|
if (rule.dayOfWeek && rule.dayOfWeek !== dayOfWeek) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rule.startTime || !rule.endTime) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return slotStart >= toMinutes(rule.startTime) && slotEnd <= toMinutes(rule.endTime);
|
||||||
|
})
|
||||||
|
.sort((first, second) => {
|
||||||
|
const firstSpecificity =
|
||||||
|
(first.dayOfWeek ? 2 : 0) + (first.startTime && first.endTime ? 1 : 0);
|
||||||
|
const secondSpecificity =
|
||||||
|
(second.dayOfWeek ? 2 : 0) + (second.startTime && second.endTime ? 1 : 0);
|
||||||
|
|
||||||
|
return secondSpecificity - firstSpecificity;
|
||||||
|
});
|
||||||
|
|
||||||
|
return Number(matchingRules[0]?.price ?? court.basePrice);
|
||||||
|
}
|
||||||
|
|
||||||
function mapBookingResponse(input: {
|
function mapBookingResponse(input: {
|
||||||
bookingId: string;
|
bookingId: string;
|
||||||
bookingCode: string;
|
bookingCode: string;
|
||||||
@@ -235,6 +277,8 @@ function mapBookingResponse(input: {
|
|||||||
endTime: string;
|
endTime: string;
|
||||||
customerName: string;
|
customerName: string;
|
||||||
customerPhone: string;
|
customerPhone: string;
|
||||||
|
customerEmail: string;
|
||||||
|
price: number;
|
||||||
status: 'CONFIRMED' | 'CANCELLED' | 'COMPLETED' | 'NOSHOW';
|
status: 'CONFIRMED' | 'CANCELLED' | 'COMPLETED' | 'NOSHOW';
|
||||||
court: {
|
court: {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -263,7 +307,9 @@ function mapBookingResponse(input: {
|
|||||||
endTime: input.endTime,
|
endTime: input.endTime,
|
||||||
customerName: input.customerName,
|
customerName: input.customerName,
|
||||||
customerPhone: input.customerPhone,
|
customerPhone: input.customerPhone,
|
||||||
|
customerEmail: input.customerEmail,
|
||||||
status: input.status,
|
status: input.status,
|
||||||
|
price: input.price,
|
||||||
createdAt: input.createdAt.toISOString(),
|
createdAt: input.createdAt.toISOString(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -284,11 +330,17 @@ export async function getPublicBookingConfirmation(complexSlug: string, bookingC
|
|||||||
bookingDate: true,
|
bookingDate: true,
|
||||||
startTime: true,
|
startTime: true,
|
||||||
endTime: true,
|
endTime: true,
|
||||||
|
customerEmail: true,
|
||||||
status: true,
|
status: true,
|
||||||
createdAt: true,
|
createdAt: true,
|
||||||
court: {
|
court: {
|
||||||
select: {
|
select: {
|
||||||
name: true,
|
name: true,
|
||||||
|
basePrice: true,
|
||||||
|
priceRules: {
|
||||||
|
where: { isActive: true },
|
||||||
|
orderBy: [{ dayOfWeek: 'asc' }, { startTime: 'asc' }],
|
||||||
|
},
|
||||||
sport: {
|
sport: {
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
@@ -299,6 +351,7 @@ export async function getPublicBookingConfirmation(complexSlug: string, bookingC
|
|||||||
complex: {
|
complex: {
|
||||||
select: {
|
select: {
|
||||||
complexName: true,
|
complexName: true,
|
||||||
|
physicalAddress: true,
|
||||||
complexSlug: true,
|
complexSlug: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -311,13 +364,22 @@ export async function getPublicBookingConfirmation(complexSlug: string, bookingC
|
|||||||
throw new PublicBookingServiceError('Reserva no encontrada.', 404);
|
throw new PublicBookingServiceError('Reserva no encontrada.', 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const date = formatIsoDate(booking.bookingDate);
|
||||||
|
const { dayOfWeek } = parseIsoDate(date);
|
||||||
|
const price = resolveSlotPrice(booking.court, dayOfWeek, {
|
||||||
|
startTime: booking.startTime,
|
||||||
|
endTime: booking.endTime,
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
bookingCode: booking.bookingCode,
|
bookingCode: booking.bookingCode,
|
||||||
complexName: booking.court.complex.complexName,
|
complexName: booking.court.complex.complexName,
|
||||||
|
complexAddress: booking.court.complex.physicalAddress ?? undefined,
|
||||||
complexSlug: booking.court.complex.complexSlug,
|
complexSlug: booking.court.complex.complexSlug,
|
||||||
date: formatIsoDate(booking.bookingDate),
|
date,
|
||||||
startTime: booking.startTime,
|
startTime: booking.startTime,
|
||||||
endTime: booking.endTime,
|
endTime: booking.endTime,
|
||||||
|
price,
|
||||||
courtName: booking.court.name,
|
courtName: booking.court.name,
|
||||||
sport: {
|
sport: {
|
||||||
id: booking.court.sport.id,
|
id: booking.court.sport.id,
|
||||||
@@ -325,13 +387,39 @@ export async function getPublicBookingConfirmation(complexSlug: string, bookingC
|
|||||||
slug: booking.court.sport.slug,
|
slug: booking.court.sport.slug,
|
||||||
},
|
},
|
||||||
status: booking.status,
|
status: booking.status,
|
||||||
|
customerEmail: booking.customerEmail,
|
||||||
createdAt: booking.createdAt.toISOString(),
|
createdAt: booking.createdAt.toISOString(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function hasVerifiedAdmin(complexId: string): Promise<boolean> {
|
||||||
|
const verifiedAdmin = await db.complexUser.findFirst({
|
||||||
|
where: {
|
||||||
|
complexId,
|
||||||
|
role: 'ADMIN',
|
||||||
|
user: { emailVerified: true },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return verifiedAdmin !== null;
|
||||||
|
}
|
||||||
|
|
||||||
export async function listPublicAvailability(complexSlug: string, query: PublicAvailabilityQuery) {
|
export async function listPublicAvailability(complexSlug: string, query: PublicAvailabilityQuery) {
|
||||||
const { bookingDate, dayOfWeek } = parseIsoDate(query.date);
|
const { bookingDate, dayOfWeek } = parseIsoDate(query.date);
|
||||||
const complex = await getComplexWithBookingData(complexSlug);
|
const complex = await getComplexWithBookingData(complexSlug);
|
||||||
|
|
||||||
|
if (!(await hasVerifiedAdmin(complex.id))) {
|
||||||
|
return {
|
||||||
|
complexId: complex.id,
|
||||||
|
complexName: complex.complexName,
|
||||||
|
complexAddress: complex.physicalAddress ?? null,
|
||||||
|
complexSlug: complex.complexSlug,
|
||||||
|
date: query.date,
|
||||||
|
sportSelectionRequired: false,
|
||||||
|
sports: [],
|
||||||
|
courts: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const sports = resolveSports(complex);
|
const sports = resolveSports(complex);
|
||||||
const sportSelectionRequired = sports.length > 1;
|
const sportSelectionRequired = sports.length > 1;
|
||||||
|
|
||||||
@@ -407,7 +495,10 @@ export async function listPublicAvailability(complexSlug: string, query: PublicA
|
|||||||
},
|
},
|
||||||
slotDurationMinutes: court.slotDurationMinutes,
|
slotDurationMinutes: court.slotDurationMinutes,
|
||||||
availabilityDay: dayOfWeek,
|
availabilityDay: dayOfWeek,
|
||||||
availableSlots,
|
availableSlots: availableSlots.map((slot) => ({
|
||||||
|
...slot,
|
||||||
|
price: resolveSlotPrice(court, dayOfWeek, slot),
|
||||||
|
})),
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.filter((court) => court.availableSlots.length > 0);
|
.filter((court) => court.availableSlots.length > 0);
|
||||||
@@ -427,6 +518,14 @@ export async function listPublicAvailability(complexSlug: string, query: PublicA
|
|||||||
export async function createPublicBooking(complexSlug: string, input: CreatePublicBookingInput) {
|
export async function createPublicBooking(complexSlug: string, input: CreatePublicBookingInput) {
|
||||||
const { bookingDate, dayOfWeek } = parseIsoDate(input.date);
|
const { bookingDate, dayOfWeek } = parseIsoDate(input.date);
|
||||||
const complex = await getComplexWithBookingData(complexSlug);
|
const complex = await getComplexWithBookingData(complexSlug);
|
||||||
|
|
||||||
|
if (!(await hasVerifiedAdmin(complex.id))) {
|
||||||
|
throw new PublicBookingServiceError(
|
||||||
|
'El complejo no puede recibir reservas hasta que un administrador verifique su email.',
|
||||||
|
403
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const sports = resolveSports(complex);
|
const sports = resolveSports(complex);
|
||||||
const { sportSelectionRequired } = validateSportSelection(sports, input.sportId);
|
const { sportSelectionRequired } = validateSportSelection(sports, input.sportId);
|
||||||
|
|
||||||
@@ -476,6 +575,10 @@ export async function createPublicBooking(complexSlug: string, input: CreatePubl
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isSlotInPast(bookingDate, input.startTime, selectedCourt.slotDurationMinutes)) {
|
||||||
|
throw new PublicBookingServiceError('No se pueden crear reservas en el pasado.', 400);
|
||||||
|
}
|
||||||
|
|
||||||
for (let attempt = 0; attempt < 5; attempt += 1) {
|
for (let attempt = 0; attempt < 5; attempt += 1) {
|
||||||
try {
|
try {
|
||||||
const booking = await db.$transaction(async (tx) => {
|
const booking = await db.$transaction(async (tx) => {
|
||||||
@@ -533,6 +636,7 @@ export async function createPublicBooking(complexSlug: string, input: CreatePubl
|
|||||||
endTime: selectedSlot.endTime,
|
endTime: selectedSlot.endTime,
|
||||||
customerName: input.customerName.trim(),
|
customerName: input.customerName.trim(),
|
||||||
customerPhone: input.customerPhone.trim(),
|
customerPhone: input.customerPhone.trim(),
|
||||||
|
customerEmail: input.customerEmail.trim(),
|
||||||
status: 'CONFIRMED',
|
status: 'CONFIRMED',
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
@@ -544,11 +648,14 @@ export async function createPublicBooking(complexSlug: string, input: CreatePubl
|
|||||||
endTime: true,
|
endTime: true,
|
||||||
customerName: true,
|
customerName: true,
|
||||||
customerPhone: true,
|
customerPhone: true,
|
||||||
|
customerEmail: true,
|
||||||
status: true,
|
status: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const price = resolveSlotPrice(selectedCourt, dayOfWeek, selectedSlot);
|
||||||
|
|
||||||
return mapBookingResponse({
|
return mapBookingResponse({
|
||||||
bookingId: booking.id,
|
bookingId: booking.id,
|
||||||
bookingCode: booking.bookingCode,
|
bookingCode: booking.bookingCode,
|
||||||
@@ -558,6 +665,8 @@ export async function createPublicBooking(complexSlug: string, input: CreatePubl
|
|||||||
endTime: booking.endTime,
|
endTime: booking.endTime,
|
||||||
customerName: booking.customerName,
|
customerName: booking.customerName,
|
||||||
customerPhone: booking.customerPhone,
|
customerPhone: booking.customerPhone,
|
||||||
|
customerEmail: booking.customerEmail,
|
||||||
|
price,
|
||||||
status: booking.status,
|
status: booking.status,
|
||||||
court: {
|
court: {
|
||||||
id: selectedCourt.id,
|
id: selectedCourt.id,
|
||||||
@@ -608,3 +717,108 @@ export async function createPublicBooking(complexSlug: string, input: CreatePubl
|
|||||||
409
|
409
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function cancelPublicBooking(complexSlug: string, input: CancelPublicBookingInput) {
|
||||||
|
const normalizedBookingCode = input.bookingCode.toUpperCase();
|
||||||
|
const booking = await db.courtBooking.findFirst({
|
||||||
|
where: {
|
||||||
|
bookingCode: normalizedBookingCode,
|
||||||
|
court: {
|
||||||
|
complex: {
|
||||||
|
complexSlug,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
court: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
basePrice: true,
|
||||||
|
priceRules: {
|
||||||
|
where: { isActive: true },
|
||||||
|
orderBy: [{ dayOfWeek: 'asc' }, { startTime: 'asc' }],
|
||||||
|
},
|
||||||
|
sport: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
slug: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
complex: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
complexName: true,
|
||||||
|
complexSlug: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!booking) {
|
||||||
|
throw new PublicBookingServiceError('Reserva no encontrada.', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (booking.status !== 'CONFIRMED') {
|
||||||
|
throw new PublicBookingServiceError(
|
||||||
|
'Solo se pueden cancelar reservas en estado confirmada.',
|
||||||
|
409
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (booking.customerPhone !== input.customerPhone.trim()) {
|
||||||
|
throw new PublicBookingServiceError('Los datos ingresados no coinciden con la reserva.', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
const date = formatIsoDate(booking.bookingDate);
|
||||||
|
const { dayOfWeek } = parseIsoDate(date);
|
||||||
|
const price = resolveSlotPrice(booking.court, dayOfWeek, {
|
||||||
|
startTime: booking.startTime,
|
||||||
|
endTime: booking.endTime,
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.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',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.courtBooking.delete({
|
||||||
|
where: { id: booking.id },
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapBookingResponse({
|
||||||
|
bookingId: booking.id,
|
||||||
|
bookingCode: booking.bookingCode,
|
||||||
|
bookingDate: booking.bookingDate,
|
||||||
|
createdAt: booking.createdAt,
|
||||||
|
startTime: booking.startTime,
|
||||||
|
endTime: booking.endTime,
|
||||||
|
customerName: booking.customerName,
|
||||||
|
customerPhone: booking.customerPhone,
|
||||||
|
customerEmail: booking.customerEmail,
|
||||||
|
price,
|
||||||
|
status: 'CANCELLED',
|
||||||
|
court: {
|
||||||
|
id: booking.court.id,
|
||||||
|
name: booking.court.name,
|
||||||
|
complexId: booking.court.complex.id,
|
||||||
|
complexName: booking.court.complex.complexName,
|
||||||
|
complexSlug: booking.court.complex.complexSlug,
|
||||||
|
sport: booking.court.sport,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { adminBookingRoutes } from '@/modules/admin-booking/admin-booking.routes';
|
import { adminBookingRoutes } from '@/modules/admin-booking/admin-booking.routes';
|
||||||
|
import { adminRoutes } from '@/modules/admin/admin.routes';
|
||||||
import { authRoutes } from '@/modules/auth/auth.routes';
|
import { authRoutes } from '@/modules/auth/auth.routes';
|
||||||
import { complexRoutes } from '@/modules/complex/complex.routes';
|
import { complexRoutes } from '@/modules/complex/complex.routes';
|
||||||
import { courtRoutes } from '@/modules/court/court.routes';
|
import { courtRoutes } from '@/modules/court/court.routes';
|
||||||
@@ -17,6 +18,7 @@ import { healthCheckRoutes } from './modules/health-check/health-check.routes';
|
|||||||
export function registerRoutes(app: Hono<AppEnv>) {
|
export function registerRoutes(app: Hono<AppEnv>) {
|
||||||
app
|
app
|
||||||
.route('', authRoutes)
|
.route('', authRoutes)
|
||||||
|
.route('/api/admin', adminRoutes)
|
||||||
.route('/api/user', userRoutes)
|
.route('/api/user', userRoutes)
|
||||||
.route('/api/plans', planRoutes)
|
.route('/api/plans', planRoutes)
|
||||||
.route('/api/password-reset', passwordResetRoutes)
|
.route('/api/password-reset', passwordResetRoutes)
|
||||||
|
|||||||
88
apps/backend/src/services/booking-email.service.ts
Normal file
88
apps/backend/src/services/booking-email.service.ts
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import {
|
||||||
|
bookingCancelledHtml,
|
||||||
|
bookingConfirmationHtml,
|
||||||
|
bookingNoShowHtml,
|
||||||
|
} from '@/emails/booking-confirmation';
|
||||||
|
import { sendMail } from '@/lib/mailer';
|
||||||
|
|
||||||
|
type BookingEmailInput = {
|
||||||
|
bookingCode: string;
|
||||||
|
complexSlug?: string;
|
||||||
|
complexName: string;
|
||||||
|
date: string;
|
||||||
|
startTime: string;
|
||||||
|
endTime: string;
|
||||||
|
courtName: string;
|
||||||
|
sportName: string;
|
||||||
|
customerName: string;
|
||||||
|
customerEmail: string;
|
||||||
|
price?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function sendBookingConfirmation(data: BookingEmailInput) {
|
||||||
|
if (!data.customerEmail) return;
|
||||||
|
|
||||||
|
const html = bookingConfirmationHtml({
|
||||||
|
bookingCode: data.bookingCode,
|
||||||
|
complexSlug: data.complexSlug,
|
||||||
|
complexName: data.complexName,
|
||||||
|
date: data.date,
|
||||||
|
startTime: data.startTime,
|
||||||
|
endTime: data.endTime,
|
||||||
|
courtName: data.courtName,
|
||||||
|
sportName: data.sportName,
|
||||||
|
customerName: data.customerName,
|
||||||
|
price: data.price,
|
||||||
|
});
|
||||||
|
|
||||||
|
await sendMail({
|
||||||
|
to: data.customerEmail,
|
||||||
|
subject: `Reserva confirmada en ${data.complexName} | Playzer`,
|
||||||
|
html,
|
||||||
|
text: `Reserva confirmada en ${data.complexName}. Codigo: ${data.bookingCode}. Cancha: ${data.courtName}. Fecha: ${data.date}. Horario: ${data.startTime} - ${data.endTime}.`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function sendBookingCancelled(data: BookingEmailInput) {
|
||||||
|
if (!data.customerEmail) return;
|
||||||
|
|
||||||
|
const html = bookingCancelledHtml({
|
||||||
|
bookingCode: data.bookingCode,
|
||||||
|
complexName: data.complexName,
|
||||||
|
date: data.date,
|
||||||
|
startTime: data.startTime,
|
||||||
|
endTime: data.endTime,
|
||||||
|
courtName: data.courtName,
|
||||||
|
sportName: data.sportName,
|
||||||
|
customerName: data.customerName,
|
||||||
|
});
|
||||||
|
|
||||||
|
await sendMail({
|
||||||
|
to: data.customerEmail,
|
||||||
|
subject: `Reserva cancelada en ${data.complexName} | Playzer`,
|
||||||
|
html,
|
||||||
|
text: `Reserva cancelada en ${data.complexName}. Codigo: ${data.bookingCode}. Fecha: ${data.date}. Horario: ${data.startTime} - ${data.endTime}.`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function sendBookingNoShow(data: BookingEmailInput) {
|
||||||
|
if (!data.customerEmail) return;
|
||||||
|
|
||||||
|
const html = bookingNoShowHtml({
|
||||||
|
bookingCode: data.bookingCode,
|
||||||
|
complexName: data.complexName,
|
||||||
|
date: data.date,
|
||||||
|
startTime: data.startTime,
|
||||||
|
endTime: data.endTime,
|
||||||
|
courtName: data.courtName,
|
||||||
|
sportName: data.sportName,
|
||||||
|
customerName: data.customerName,
|
||||||
|
});
|
||||||
|
|
||||||
|
await sendMail({
|
||||||
|
to: data.customerEmail,
|
||||||
|
subject: `Reserva no concretada en ${data.complexName} | Playzer`,
|
||||||
|
html,
|
||||||
|
text: `Reserva no concretada en ${data.complexName}. Codigo: ${data.bookingCode}. Fecha: ${data.date}. Horario: ${data.startTime} - ${data.endTime}.`,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import { beforeEach, expect, mock, test } from 'bun:test';
|
import { beforeEach, expect, mock, test } from 'bun:test';
|
||||||
|
|
||||||
import { createInviteComplexUserHandler } from '@/modules/complex/handlers/invite-complex-user.handler';
|
|
||||||
import type { InviteComplexUserResponse } from '@repo/api-contract';
|
import type { InviteComplexUserResponse } from '@repo/api-contract';
|
||||||
|
|
||||||
const inviteComplexUserMock = mock(async () => undefined as unknown as InviteComplexUserResponse);
|
const inviteComplexUserMock = mock(async () => undefined as unknown as InviteComplexUserResponse);
|
||||||
@@ -15,6 +14,10 @@ class MockComplexMembersError extends Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { createInviteComplexUserHandler } = await import(
|
||||||
|
'@/modules/complex/handlers/invite-complex-user.handler'
|
||||||
|
);
|
||||||
|
|
||||||
const inviteComplexUserHandler = createInviteComplexUserHandler({
|
const inviteComplexUserHandler = createInviteComplexUserHandler({
|
||||||
inviteComplexUser: inviteComplexUserMock,
|
inviteComplexUser: inviteComplexUserMock,
|
||||||
ComplexMembersError: MockComplexMembersError,
|
ComplexMembersError: MockComplexMembersError,
|
||||||
@@ -59,7 +62,6 @@ function createContext(input: {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
inviteComplexUserMock.mockReset();
|
inviteComplexUserMock.mockReset();
|
||||||
mock.clearAllMocks();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('returns 201 with the service result', async () => {
|
test('returns 201 with the service result', async () => {
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
import { beforeEach, expect, mock, test } from 'bun:test';
|
import { beforeEach, expect, test } from 'bun:test';
|
||||||
|
|
||||||
import {
|
|
||||||
ComplexMembersError,
|
|
||||||
inviteComplexUser,
|
|
||||||
} from '@/modules/complex/services/complex-members.service';
|
|
||||||
import { prismaMock, sendMailMock, transactionMock } from '../support/prisma.mock';
|
import { prismaMock, sendMailMock, transactionMock } from '../support/prisma.mock';
|
||||||
|
|
||||||
|
const { ComplexMembersError, inviteComplexUser } = await import(
|
||||||
|
'@/modules/complex/services/complex-members.service'
|
||||||
|
);
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
prismaMock._reset();
|
prismaMock._reset();
|
||||||
mock.clearAllMocks();
|
sendMailMock.mockClear();
|
||||||
|
transactionMock.mockClear();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('creates a pending invitation and sends the email', async () => {
|
test('creates a pending invitation and sends the email', async () => {
|
||||||
@@ -21,10 +22,10 @@ test('creates a pending invitation and sends the email', async () => {
|
|||||||
complexId: 'complex-1',
|
complexId: 'complex-1',
|
||||||
email: 'new.member@example.com',
|
email: 'new.member@example.com',
|
||||||
tokenHash: 'token-hash',
|
tokenHash: 'token-hash',
|
||||||
expiresAt: new Date('2026-04-29T00:00:00.000Z'),
|
expiresAt: new Date('2027-01-01T00:00:00.000Z'),
|
||||||
acceptedAt: null,
|
acceptedAt: null,
|
||||||
revokedAt: null,
|
revokedAt: null,
|
||||||
createdAt: new Date('2026-04-22T00:00:00.000Z'),
|
createdAt: new Date('2026-12-01T00:00:00.000Z'),
|
||||||
} as never);
|
} as never);
|
||||||
|
|
||||||
const result = await inviteComplexUser('admin-1', 'complex-1', {
|
const result = await inviteComplexUser('admin-1', 'complex-1', {
|
||||||
@@ -79,7 +80,7 @@ test('rejects when the invite target already belongs to the complex', async () =
|
|||||||
}
|
}
|
||||||
|
|
||||||
expect(caught).toBeInstanceOf(ComplexMembersError);
|
expect(caught).toBeInstanceOf(ComplexMembersError);
|
||||||
expect((caught as ComplexMembersError).status).toBe(409);
|
expect((caught as InstanceType<typeof ComplexMembersError>).status).toBe(409);
|
||||||
expect(transactionMock).not.toHaveBeenCalled();
|
expect(transactionMock).not.toHaveBeenCalled();
|
||||||
expect(sendMailMock).not.toHaveBeenCalled();
|
expect(sendMailMock).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|||||||
89
apps/backend/test/plan/plan-pricing.service.test.ts
Normal file
89
apps/backend/test/plan/plan-pricing.service.test.ts
Normal 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',
|
||||||
|
});
|
||||||
|
});
|
||||||
90
apps/backend/test/public-booking/slot-validator.test.ts
Normal file
90
apps/backend/test/public-booking/slot-validator.test.ts
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import { expect, test } from 'bun:test';
|
||||||
|
|
||||||
|
import { isSlotInPast } from '@/lib/slot-validator';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to create a bookingDate like parseIsoDate does (UTC midnight).
|
||||||
|
*/
|
||||||
|
function bookingDate(year: number, month: number, day: number): Date {
|
||||||
|
return new Date(Date.UTC(year, month - 1, day));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to create a "now" date at a specific local time.
|
||||||
|
* Using local date constructor guarantees the date parts
|
||||||
|
* (getFullYear, getMonth, getDate) match the arguments
|
||||||
|
* regardless of the test runner's timezone.
|
||||||
|
*/
|
||||||
|
function localDate(year: number, month: number, day: number, hours: number, minutes: number): Date {
|
||||||
|
return new Date(year, month - 1, day, hours, minutes, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
test('future date — always allowed', () => {
|
||||||
|
const future = bookingDate(2027, 6, 10); // 2027-06-10
|
||||||
|
const now = localDate(2026, 6, 8, 12, 0); // 2026-06-08 12:00
|
||||||
|
|
||||||
|
expect(isSlotInPast(future, '10:00', 60, now)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('past date — rejected', () => {
|
||||||
|
const past = bookingDate(2025, 6, 8); // 2025-06-08
|
||||||
|
const now = localDate(2026, 6, 8, 12, 0); // 2026-06-08 12:00
|
||||||
|
|
||||||
|
expect(isSlotInPast(past, '10:00', 60, now)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('today, slot not started yet — allowed', () => {
|
||||||
|
const today = bookingDate(2026, 6, 8); // 2026-06-08
|
||||||
|
const now = localDate(2026, 6, 8, 9, 0); // 09:00, slot empieza a las 10:00
|
||||||
|
|
||||||
|
expect(isSlotInPast(today, '10:00', 60, now)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('today, slot in progress less than half elapsed — allowed', () => {
|
||||||
|
const today = bookingDate(2026, 6, 8);
|
||||||
|
const now = localDate(2026, 6, 8, 10, 10); // 10:10, slot 10:00-11:00 (60 min), 10 min elapsed
|
||||||
|
|
||||||
|
expect(isSlotInPast(today, '10:00', 60, now)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('today, slot in progress more than half elapsed — rejected', () => {
|
||||||
|
const today = bookingDate(2026, 6, 8);
|
||||||
|
const now = localDate(2026, 6, 8, 10, 40); // 10:40, slot 10:00-11:00 (60 min), 40 min elapsed
|
||||||
|
|
||||||
|
expect(isSlotInPast(today, '10:00', 60, now)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('today, slot already ended — rejected', () => {
|
||||||
|
const today = bookingDate(2026, 6, 8);
|
||||||
|
const now = localDate(2026, 6, 8, 11, 1); // 11:01, slot 10:00-11:00 terminó
|
||||||
|
|
||||||
|
expect(isSlotInPast(today, '10:00', 60, now)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('today, exactly half elapsed — allowed (inclusive boundary)', () => {
|
||||||
|
const today = bookingDate(2026, 6, 8);
|
||||||
|
const now = localDate(2026, 6, 8, 10, 30); // 10:30, slot 10:00-11:00 (60 min), exactamente 30 min
|
||||||
|
|
||||||
|
expect(isSlotInPast(today, '10:00', 60, now)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('today, slot starts exactly now — allowed', () => {
|
||||||
|
const today = bookingDate(2026, 6, 8);
|
||||||
|
const now = localDate(2026, 6, 8, 10, 0); // 10:00 exacto
|
||||||
|
|
||||||
|
expect(isSlotInPast(today, '10:00', 60, now)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('90 min slot with 10 min elapsed — allowed', () => {
|
||||||
|
const today = bookingDate(2026, 6, 8);
|
||||||
|
const now = localDate(2026, 6, 8, 12, 10); // 12:10, slot 12:00-13:30, 10/90 elapsed
|
||||||
|
|
||||||
|
expect(isSlotInPast(today, '12:00', 90, now)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('90 min slot with 50 min elapsed — rejected', () => {
|
||||||
|
const today = bookingDate(2026, 6, 8);
|
||||||
|
const now = localDate(2026, 6, 8, 12, 50); // 12:50, slot 12:00-13:30, 50/90 elapsed
|
||||||
|
|
||||||
|
expect(isSlotInPast(today, '12:00', 90, now)).toBe(true);
|
||||||
|
});
|
||||||
@@ -49,7 +49,6 @@ function createContext(body: unknown) {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
createSportMock.mockReset();
|
createSportMock.mockReset();
|
||||||
mock.clearAllMocks();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('returns 201 with the created sport', async () => {
|
test('returns 201 with the created sport', async () => {
|
||||||
|
|||||||
@@ -1,24 +1,6 @@
|
|||||||
import { beforeEach, expect, mock, test } from 'bun:test';
|
import { beforeEach, expect, test } from 'bun:test';
|
||||||
|
|
||||||
import { createPrismaMock } from 'bun-mock-prisma';
|
import { prismaMock, uuidV7Mock } from '../support/prisma.mock';
|
||||||
|
|
||||||
import type { PrismaClient } from '@/generated/prisma/client';
|
|
||||||
import type { PrismaClientMock } from 'bun-mock-prisma';
|
|
||||||
|
|
||||||
const prismaMock = createPrismaMock<PrismaClient>() as PrismaClientMock<PrismaClient>;
|
|
||||||
const uuidV7Mock = mock(() => '00000000-0000-4000-8000-000000000001');
|
|
||||||
|
|
||||||
mock.module('@/lib/prisma', () => ({
|
|
||||||
__esModule: true,
|
|
||||||
db: {
|
|
||||||
sport: prismaMock.sport,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
mock.module('uuid', () => ({
|
|
||||||
__esModule: true,
|
|
||||||
v7: uuidV7Mock,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const { createSport, getSportById, listSports, updateSport } = await import(
|
const { createSport, getSportById, listSports, updateSport } = await import(
|
||||||
'@/modules/sport/services/sport.service'
|
'@/modules/sport/services/sport.service'
|
||||||
@@ -26,7 +8,7 @@ const { createSport, getSportById, listSports, updateSport } = await import(
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
prismaMock._reset();
|
prismaMock._reset();
|
||||||
mock.clearAllMocks();
|
uuidV7Mock.mockClear();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('listSports returns sports ordered by name', async () => {
|
test('listSports returns sports ordered by name', async () => {
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export const dbMock = {
|
|||||||
complex: prismaMock.complex,
|
complex: prismaMock.complex,
|
||||||
user: prismaMock.user,
|
user: prismaMock.user,
|
||||||
complexInvitation: prismaMock.complexInvitation,
|
complexInvitation: prismaMock.complexInvitation,
|
||||||
|
sport: prismaMock.sport,
|
||||||
$transaction: transactionMock,
|
$transaction: transactionMock,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -33,3 +34,10 @@ mock.module('@/lib/mailer', () => ({
|
|||||||
__esModule: true,
|
__esModule: true,
|
||||||
sendMail: sendMailMock,
|
sendMail: sendMailMock,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
export const uuidV7Mock = mock(() => '00000000-0000-4000-8000-000000000001');
|
||||||
|
|
||||||
|
mock.module('uuid', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
v7: uuidV7Mock,
|
||||||
|
}));
|
||||||
|
|||||||
BIN
apps/frontend/public/playzer-favicon-512-transparent.png
Normal file
BIN
apps/frontend/public/playzer-favicon-512-transparent.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 151 KiB |
@@ -6,6 +6,7 @@ import {
|
|||||||
Menu,
|
Menu,
|
||||||
Moon,
|
Moon,
|
||||||
Settings,
|
Settings,
|
||||||
|
Shield,
|
||||||
Sun,
|
Sun,
|
||||||
User,
|
User,
|
||||||
X,
|
X,
|
||||||
@@ -241,6 +242,18 @@ export function Header() {
|
|||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{user?.role === 'super_admin' && (
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="cursor-pointer rounded-xl"
|
||||||
|
onSelect={() => {
|
||||||
|
void navigate({ to: '/admin' });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Shield className="mr-2 size-4 text-emerald-600" />
|
||||||
|
Admin
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
|
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
@@ -405,6 +418,20 @@ export function Header() {
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{user?.role === 'super_admin' && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="justify-start rounded-2xl"
|
||||||
|
onClick={() => {
|
||||||
|
setOpen(false);
|
||||||
|
void navigate({ to: '/admin' });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Shield className="mr-2 size-4 text-emerald-600" />
|
||||||
|
Admin
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
{isAuthenticated && (
|
{isAuthenticated && (
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
|||||||
31
apps/frontend/src/components/ui/badge.tsx
Normal file
31
apps/frontend/src/components/ui/badge.tsx
Normal 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 };
|
||||||
@@ -35,7 +35,7 @@ function SelectTrigger({
|
|||||||
data-slot="select-trigger"
|
data-slot="select-trigger"
|
||||||
data-size={size}
|
data-size={size}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex w-fit items-center justify-between gap-1.5 rounded-lg border border-input bg-transparent py-2 pr-2 pl-2.5 text-sm whitespace-nowrap transition-colors outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-placeholder:text-muted-foreground data-[size=default]:h-8 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
"flex w-full items-center justify-between gap-1.5 rounded-lg border border-input bg-transparent py-2 pr-2 pl-2.5 text-sm whitespace-nowrap transition-colors outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-placeholder:text-muted-foreground data-[size=default]:h-8 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
68
apps/frontend/src/features/admin/admin-layout.tsx
Normal file
68
apps/frontend/src/features/admin/admin-layout.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
254
apps/frontend/src/features/admin/admin-page.tsx
Normal file
254
apps/frontend/src/features/admin/admin-page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
167
apps/frontend/src/features/admin/complexes-page.tsx
Normal file
167
apps/frontend/src/features/admin/complexes-page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
810
apps/frontend/src/features/admin/plans-page.tsx
Normal file
810
apps/frontend/src/features/admin/plans-page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
467
apps/frontend/src/features/admin/users-page.tsx
Normal file
467
apps/frontend/src/features/admin/users-page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,11 @@
|
|||||||
import { ApiClientError, apiClient } from '@/lib/api-client';
|
import { ApiClientError, apiClient } from '@/lib/api-client';
|
||||||
import type { AdminBooking, ComplexWithRole, Court } from '@repo/api-contract';
|
import type {
|
||||||
|
AdminBooking,
|
||||||
|
ComplexWithRole,
|
||||||
|
Court,
|
||||||
|
PlanFeatureFlags,
|
||||||
|
RecurringBookingGroup,
|
||||||
|
} from '@repo/api-contract';
|
||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
import {
|
import {
|
||||||
type ReactNode,
|
type ReactNode,
|
||||||
@@ -41,6 +47,17 @@ interface CreateManualBookingPayload {
|
|||||||
startTime: string;
|
startTime: string;
|
||||||
customerName: string;
|
customerName: string;
|
||||||
customerPhone: string;
|
customerPhone: string;
|
||||||
|
customerEmail: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CreateRecurringBookingPayload {
|
||||||
|
courtId: string;
|
||||||
|
date: string;
|
||||||
|
startTime: string;
|
||||||
|
customerName: string;
|
||||||
|
customerPhone: string;
|
||||||
|
customerEmail: string;
|
||||||
|
recurringEndDate?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BookingContextValue {
|
interface BookingContextValue {
|
||||||
@@ -65,6 +82,8 @@ interface BookingContextValue {
|
|||||||
createBookingError: string | null;
|
createBookingError: string | null;
|
||||||
isCreatingBooking: boolean;
|
isCreatingBooking: boolean;
|
||||||
bookingToolsOpen: boolean;
|
bookingToolsOpen: boolean;
|
||||||
|
planFeatures: PlanFeatureFlags | null;
|
||||||
|
isRecurringEnabled: boolean;
|
||||||
setSelectedDate: (date: string) => void;
|
setSelectedDate: (date: string) => void;
|
||||||
moveSelectedDate: (amount: number) => void;
|
moveSelectedDate: (amount: number) => void;
|
||||||
setSelectedSportId: (sportId: string) => void;
|
setSelectedSportId: (sportId: string) => void;
|
||||||
@@ -72,8 +91,14 @@ interface BookingContextValue {
|
|||||||
setViewMode: (mode: BookingViewMode) => void;
|
setViewMode: (mode: BookingViewMode) => void;
|
||||||
setVisibleTimeRange: (range: BookingTimeRange) => void;
|
setVisibleTimeRange: (range: BookingTimeRange) => void;
|
||||||
openCreateBooking: (draft?: Partial<BookingSlotDraft>) => void;
|
openCreateBooking: (draft?: Partial<BookingSlotDraft>) => void;
|
||||||
|
recurringGroups: RecurringBookingGroup[];
|
||||||
|
isRecurringGroupsLoading: boolean;
|
||||||
|
cancelRecurringGroup: (groupId: string) => void;
|
||||||
|
isCancellingRecurringGroup: boolean;
|
||||||
|
refreshRecurringGroups: () => void;
|
||||||
closeCreateBooking: () => void;
|
closeCreateBooking: () => void;
|
||||||
createBooking: (payload: CreateManualBookingPayload) => Promise<void>;
|
createBooking: (payload: CreateManualBookingPayload) => Promise<void>;
|
||||||
|
createRecurringBooking: (payload: CreateRecurringBookingPayload) => Promise<void>;
|
||||||
updateBookingStatus: (bookingId: string, status: 'CANCELLED' | 'COMPLETED' | 'NOSHOW') => void;
|
updateBookingStatus: (bookingId: string, status: 'CANCELLED' | 'COMPLETED' | 'NOSHOW') => void;
|
||||||
exportDayReport: () => void;
|
exportDayReport: () => void;
|
||||||
openBookingTools: (segment?: BookingTimelineSegment) => void;
|
openBookingTools: (segment?: BookingTimelineSegment) => void;
|
||||||
@@ -146,6 +171,9 @@ function buildSegmentsForCourt(
|
|||||||
const segments: BookingTimelineSegment[] = [];
|
const segments: BookingTimelineSegment[] = [];
|
||||||
const slotDuration = court.slotDurationMinutes;
|
const slotDuration = court.slotDurationMinutes;
|
||||||
|
|
||||||
|
const nowMinutes = timeToMinutes(getNowTime());
|
||||||
|
const isToday = isTodayIso(selectedDate);
|
||||||
|
|
||||||
const availabilityRanges = court.availability
|
const availabilityRanges = court.availability
|
||||||
.filter((range) => range.dayOfWeek === dayOfWeek)
|
.filter((range) => range.dayOfWeek === dayOfWeek)
|
||||||
.map((range) => ({
|
.map((range) => ({
|
||||||
@@ -180,6 +208,9 @@ function buildSegmentsForCourt(
|
|||||||
slotStart += slotDuration
|
slotStart += slotDuration
|
||||||
) {
|
) {
|
||||||
const slotEnd = slotStart + slotDuration;
|
const slotEnd = slotStart + slotDuration;
|
||||||
|
|
||||||
|
if (isToday && slotEnd <= nowMinutes) continue;
|
||||||
|
|
||||||
const isBooked = courtBookings.some((booking) =>
|
const isBooked = courtBookings.some((booking) =>
|
||||||
overlapsBooking(slotStart, slotEnd, booking)
|
overlapsBooking(slotStart, slotEnd, booking)
|
||||||
);
|
);
|
||||||
@@ -209,20 +240,14 @@ function buildSegmentsForCourt(
|
|||||||
function getSummary(schedules: BookingCourtSchedule[]): BookingSummary {
|
function getSummary(schedules: BookingCourtSchedule[]): BookingSummary {
|
||||||
const freeSlots = schedules.reduce((total, schedule) => total + schedule.metrics.free, 0);
|
const freeSlots = schedules.reduce((total, schedule) => total + schedule.metrics.free, 0);
|
||||||
const reservedSlots = schedules.reduce((total, schedule) => total + schedule.metrics.reserved, 0);
|
const reservedSlots = schedules.reduce((total, schedule) => total + schedule.metrics.reserved, 0);
|
||||||
const maintenanceSlots = schedules.reduce(
|
const totalSegments = freeSlots + reservedSlots || 1;
|
||||||
(total, schedule) => total + schedule.metrics.maintenance,
|
|
||||||
0
|
|
||||||
);
|
|
||||||
const totalSegments = freeSlots + reservedSlots + maintenanceSlots || 1;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
totalReservations: reservedSlots + maintenanceSlots + freeSlots,
|
totalReservations: freeSlots + reservedSlots,
|
||||||
freeSlots,
|
freeSlots,
|
||||||
reservedSlots,
|
reservedSlots,
|
||||||
maintenanceSlots,
|
|
||||||
freePercent: Math.round((freeSlots / totalSegments) * 100),
|
freePercent: Math.round((freeSlots / totalSegments) * 100),
|
||||||
reservedPercent: Math.round((reservedSlots / totalSegments) * 100),
|
reservedPercent: Math.round((reservedSlots / totalSegments) * 100),
|
||||||
maintenancePercent: Math.round((maintenanceSlots / totalSegments) * 100),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -244,6 +269,20 @@ export function BookingProvider({ children, complex }: BookingProviderProps) {
|
|||||||
queryFn: () => apiClient.courts.listByComplex(complex.id),
|
queryFn: () => apiClient.courts.listByComplex(complex.id),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const recurringGroupsQuery = useQuery({
|
||||||
|
queryKey: ['recurring-groups', complex.id],
|
||||||
|
queryFn: () => apiClient.adminBookings.listRecurringGroups(complex.id),
|
||||||
|
enabled: viewMode === 'recurring',
|
||||||
|
});
|
||||||
|
|
||||||
|
const cancelRecurringGroupMutation = useMutation({
|
||||||
|
mutationFn: (groupId: string) => apiClient.adminBookings.cancelRecurringGroup(groupId),
|
||||||
|
onSuccess: async () => {
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ['recurring-groups', complex.id] });
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ['admin-bookings', complex.id] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const bookingsQuery = useQuery({
|
const bookingsQuery = useQuery({
|
||||||
queryKey: ['admin-bookings', complex.id, selectedDate],
|
queryKey: ['admin-bookings', complex.id, selectedDate],
|
||||||
queryFn: () =>
|
queryFn: () =>
|
||||||
@@ -274,7 +313,6 @@ export function BookingProvider({ children, complex }: BookingProviderProps) {
|
|||||||
const metrics = {
|
const metrics = {
|
||||||
free: segments.filter((segment) => segment.status === 'free').length,
|
free: segments.filter((segment) => segment.status === 'free').length,
|
||||||
reserved: segments.filter((segment) => segment.status === 'reserved').length,
|
reserved: segments.filter((segment) => segment.status === 'reserved').length,
|
||||||
maintenance: segments.filter((segment) => segment.status === 'maintenance').length,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return { court, segments, metrics };
|
return { court, segments, metrics };
|
||||||
@@ -315,6 +353,27 @@ export function BookingProvider({ children, complex }: BookingProviderProps) {
|
|||||||
startTime: payload.startTime,
|
startTime: payload.startTime,
|
||||||
customerName: payload.customerName,
|
customerName: payload.customerName,
|
||||||
customerPhone: payload.customerPhone,
|
customerPhone: payload.customerPhone,
|
||||||
|
customerEmail: payload.customerEmail,
|
||||||
|
}),
|
||||||
|
onSuccess: async () => {
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ['admin-bookings', complex.id] });
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ['manual-booking-availability'] });
|
||||||
|
setIsCreateBookingOpen(false);
|
||||||
|
setSelectedSlot(null);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
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 () => {
|
onSuccess: async () => {
|
||||||
await queryClient.invalidateQueries({ queryKey: ['admin-bookings', complex.id] });
|
await queryClient.invalidateQueries({ queryKey: ['admin-bookings', complex.id] });
|
||||||
@@ -412,6 +471,16 @@ export function BookingProvider({ children, complex }: BookingProviderProps) {
|
|||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
}, [bookings, complex.complexSlug, selectedDate]);
|
}, [bookings, complex.complexSlug, selectedDate]);
|
||||||
|
|
||||||
|
const planFeatures = complex.planFeatures ?? null;
|
||||||
|
const isRecurringEnabled = planFeatures?.fixedSlots ?? false;
|
||||||
|
|
||||||
|
const createRecurringBooking = useCallback(
|
||||||
|
async (payload: CreateRecurringBookingPayload) => {
|
||||||
|
await createRecurringBookingMutation.mutateAsync(payload);
|
||||||
|
},
|
||||||
|
[createRecurringBookingMutation]
|
||||||
|
);
|
||||||
|
|
||||||
const value = useMemo<BookingContextValue>(
|
const value = useMemo<BookingContextValue>(
|
||||||
() => ({
|
() => ({
|
||||||
complex,
|
complex,
|
||||||
@@ -436,11 +505,24 @@ export function BookingProvider({ children, complex }: BookingProviderProps) {
|
|||||||
'No pudimos cargar el panel de reservas.'
|
'No pudimos cargar el panel de reservas.'
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
|
recurringGroups: recurringGroupsQuery.data?.groups ?? [],
|
||||||
|
isRecurringGroupsLoading: recurringGroupsQuery.isLoading,
|
||||||
|
cancelRecurringGroup: (groupId: string) => cancelRecurringGroupMutation.mutate(groupId),
|
||||||
|
isCancellingRecurringGroup: cancelRecurringGroupMutation.isPending,
|
||||||
|
refreshRecurringGroups: () =>
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['recurring-groups', complex.id] }),
|
||||||
isCreateBookingOpen,
|
isCreateBookingOpen,
|
||||||
createBookingError: createBookingMutation.isError
|
createBookingError:
|
||||||
? extractMessage(createBookingMutation.error, 'No pudimos crear la reserva.')
|
createBookingMutation.isError || createRecurringBookingMutation.isError
|
||||||
: null,
|
? extractMessage(
|
||||||
isCreatingBooking: createBookingMutation.isPending,
|
createBookingMutation.error ?? createRecurringBookingMutation.error,
|
||||||
|
'No pudimos crear la reserva.'
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
isCreatingBooking:
|
||||||
|
createBookingMutation.isPending || createRecurringBookingMutation.isPending,
|
||||||
|
planFeatures,
|
||||||
|
isRecurringEnabled,
|
||||||
setSelectedDate,
|
setSelectedDate,
|
||||||
moveSelectedDate: (amount) => setSelectedDate((date) => addDaysIso(date, amount)),
|
moveSelectedDate: (amount) => setSelectedDate((date) => addDaysIso(date, amount)),
|
||||||
setSelectedSportId,
|
setSelectedSportId,
|
||||||
@@ -450,6 +532,7 @@ export function BookingProvider({ children, complex }: BookingProviderProps) {
|
|||||||
openCreateBooking,
|
openCreateBooking,
|
||||||
closeCreateBooking,
|
closeCreateBooking,
|
||||||
createBooking,
|
createBooking,
|
||||||
|
createRecurringBooking,
|
||||||
updateBookingStatus,
|
updateBookingStatus,
|
||||||
exportDayReport,
|
exportDayReport,
|
||||||
bookingToolsOpen,
|
bookingToolsOpen,
|
||||||
@@ -472,10 +555,16 @@ export function BookingProvider({ children, complex }: BookingProviderProps) {
|
|||||||
createBookingMutation.error,
|
createBookingMutation.error,
|
||||||
createBookingMutation.isError,
|
createBookingMutation.isError,
|
||||||
createBookingMutation.isPending,
|
createBookingMutation.isPending,
|
||||||
|
createRecurringBooking,
|
||||||
|
createRecurringBookingMutation.error,
|
||||||
|
createRecurringBookingMutation.isError,
|
||||||
|
createRecurringBookingMutation.isPending,
|
||||||
currentTime,
|
currentTime,
|
||||||
exportDayReport,
|
exportDayReport,
|
||||||
isCreateBookingOpen,
|
isCreateBookingOpen,
|
||||||
openCreateBooking,
|
openCreateBooking,
|
||||||
|
planFeatures,
|
||||||
|
isRecurringEnabled,
|
||||||
schedules,
|
schedules,
|
||||||
selectedDate,
|
selectedDate,
|
||||||
selectedSlot,
|
selectedSlot,
|
||||||
@@ -487,6 +576,9 @@ export function BookingProvider({ children, complex }: BookingProviderProps) {
|
|||||||
viewMode,
|
viewMode,
|
||||||
visibleTimeRange,
|
visibleTimeRange,
|
||||||
bookingToolsOpen,
|
bookingToolsOpen,
|
||||||
|
recurringGroupsQuery.data?.groups,
|
||||||
|
recurringGroupsQuery.isLoading,
|
||||||
|
cancelRecurringGroupMutation.isPending,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,38 +1,94 @@
|
|||||||
import { useIsMobile } from '@/hooks/use-mobile';
|
import { useIsMobile } from '@/hooks/use-mobile';
|
||||||
|
import { authClient } from '@/lib/api-client';
|
||||||
|
import { useAuth } from '@/lib/auth';
|
||||||
import type { ComplexWithRole } from '@repo/api-contract';
|
import type { ComplexWithRole } from '@repo/api-contract';
|
||||||
import { BookingProvider } from './booking-provider';
|
import { useState } from 'react';
|
||||||
|
import { BookingProvider, useBooking } from './booking-provider';
|
||||||
import { BookingCreateDialog } from './components/booking-create-dialog';
|
import { BookingCreateDialog } from './components/booking-create-dialog';
|
||||||
import { BookingDaySummary, BookingQuickActions } from './components/booking-day-summary';
|
import { BookingDaySummary, BookingQuickActions } from './components/booking-day-summary';
|
||||||
import { BookingHeader } from './components/booking-header';
|
import { BookingHeader } from './components/booking-header';
|
||||||
|
import { BookingListView } from './components/booking-list-view';
|
||||||
import { BookingMobile } from './components/booking-mobile';
|
import { BookingMobile } from './components/booking-mobile';
|
||||||
import { BookingTimeline } from './components/booking-timeline';
|
import { BookingTimeline } from './components/booking-timeline';
|
||||||
import { BookingToolbar } from './components/booking-toolbar';
|
import { BookingToolbar } from './components/booking-toolbar';
|
||||||
import { BookingToolsDialog } from './components/booking-tools-dialog';
|
import { BookingToolsDialog } from './components/booking-tools-dialog';
|
||||||
|
import { RecurringBookingListView } from './components/recurring-booking-list';
|
||||||
|
|
||||||
interface BookingProps {
|
interface BookingProps {
|
||||||
complex: ComplexWithRole;
|
complex: ComplexWithRole;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Booking({ complex }: BookingProps) {
|
export function Booking({ complex }: BookingProps) {
|
||||||
const isMobile = useIsMobile();
|
const { user } = useAuth();
|
||||||
|
const [sending, setSending] = useState(false);
|
||||||
|
const [emailSent, setEmailSent] = useState(false);
|
||||||
|
|
||||||
|
const emailNotVerified = user && !user.emailVerified;
|
||||||
|
|
||||||
|
const handleResendVerification = async () => {
|
||||||
|
if (!user?.email || sending) return;
|
||||||
|
setSending(true);
|
||||||
|
try {
|
||||||
|
await authClient.sendVerificationEmail({ email: user.email });
|
||||||
|
setEmailSent(true);
|
||||||
|
setTimeout(() => setEmailSent(false), 6000);
|
||||||
|
} catch {
|
||||||
|
// Silently fail — the banner already shows the email to contact
|
||||||
|
} finally {
|
||||||
|
setSending(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BookingProvider complex={complex}>
|
<BookingProvider complex={complex}>
|
||||||
{isMobile ? (
|
{emailNotVerified && (
|
||||||
<BookingMobile />
|
<div className="mb-6 flex items-center justify-between gap-4 rounded-xl border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-800 dark:border-amber-800/30 dark:bg-amber-950/30 dark:text-amber-200">
|
||||||
) : (
|
<span>
|
||||||
<div className="flex flex-col gap-5">
|
No verificaste tu email. Para crear reservas necesitás verificar tu dirección de correo
|
||||||
<BookingHeader />
|
electrónico.
|
||||||
<BookingToolbar />
|
</span>
|
||||||
<BookingTimeline />
|
<button
|
||||||
<div className="grid gap-5 xl:grid-cols-4">
|
type="button"
|
||||||
<BookingDaySummary />
|
onClick={handleResendVerification}
|
||||||
<BookingQuickActions />
|
disabled={sending}
|
||||||
</div>
|
className="shrink-0 rounded-lg bg-amber-200 px-3 py-1.5 text-xs font-medium text-amber-900 transition-colors hover:bg-amber-300 disabled:opacity-50 dark:bg-amber-800 dark:text-amber-50 dark:hover:bg-amber-700"
|
||||||
|
>
|
||||||
|
{sending ? 'Enviando...' : emailSent ? '¡Email enviado!' : 'Reenviar email'}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<BookingInner />
|
||||||
<BookingCreateDialog />
|
<BookingCreateDialog />
|
||||||
<BookingToolsDialog />
|
<BookingToolsDialog />
|
||||||
</BookingProvider>
|
</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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import type { AdminBooking, Court } from '@repo/api-contract';
|
import type { AdminBooking, Court } from '@repo/api-contract';
|
||||||
|
|
||||||
export type BookingViewMode = 'panel' | 'status';
|
export type BookingViewMode = 'panel' | 'status' | 'list' | 'recurring';
|
||||||
export type BookingStatusFilter = 'all' | 'free' | 'reserved' | 'maintenance';
|
export type BookingStatusFilter = 'all' | 'free' | 'reserved';
|
||||||
export type BookingSegmentStatus = 'free' | 'reserved' | 'maintenance';
|
export type BookingSegmentStatus = 'free' | 'reserved';
|
||||||
|
|
||||||
export interface BookingTimeRange {
|
export interface BookingTimeRange {
|
||||||
start: string;
|
start: string;
|
||||||
@@ -13,16 +13,13 @@ export interface BookingSummary {
|
|||||||
totalReservations: number;
|
totalReservations: number;
|
||||||
freeSlots: number;
|
freeSlots: number;
|
||||||
reservedSlots: number;
|
reservedSlots: number;
|
||||||
maintenanceSlots: number;
|
|
||||||
freePercent: number;
|
freePercent: number;
|
||||||
reservedPercent: number;
|
reservedPercent: number;
|
||||||
maintenancePercent: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BookingCourtMetrics {
|
export interface BookingCourtMetrics {
|
||||||
free: number;
|
free: number;
|
||||||
reserved: number;
|
reserved: number;
|
||||||
maintenance: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BookingTimelineSegment {
|
export interface BookingTimelineSegment {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select';
|
} from '@/components/ui/select';
|
||||||
|
import { Separator } from '@/components/ui/separator';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
@@ -26,6 +27,8 @@ import { useBooking } from '../booking-provider';
|
|||||||
import {
|
import {
|
||||||
fromIsoDateLocal,
|
fromIsoDateLocal,
|
||||||
getDayOfWeek,
|
getDayOfWeek,
|
||||||
|
getNowTime,
|
||||||
|
isTodayIso,
|
||||||
minutesToTime,
|
minutesToTime,
|
||||||
timeToMinutes,
|
timeToMinutes,
|
||||||
toIsoDateLocal,
|
toIsoDateLocal,
|
||||||
@@ -42,6 +45,7 @@ const bookingFormSchema = z.object({
|
|||||||
.trim()
|
.trim()
|
||||||
.min(6, 'Ingresa un telefono válido.')
|
.min(6, 'Ingresa un telefono válido.')
|
||||||
.max(30, 'El telefono no puede superar los 30 caracteres.'),
|
.max(30, 'El telefono no puede superar los 30 caracteres.'),
|
||||||
|
customerEmail: z.string().email('Ingresá un email válido.').or(z.literal('')),
|
||||||
});
|
});
|
||||||
|
|
||||||
type BookingForm = z.infer<typeof bookingFormSchema>;
|
type BookingForm = z.infer<typeof bookingFormSchema>;
|
||||||
@@ -57,12 +61,16 @@ export function BookingCreateDialog() {
|
|||||||
isCreatingBooking,
|
isCreatingBooking,
|
||||||
closeCreateBooking,
|
closeCreateBooking,
|
||||||
createBooking,
|
createBooking,
|
||||||
|
createRecurringBooking,
|
||||||
|
isRecurringEnabled,
|
||||||
} = useBooking();
|
} = useBooking();
|
||||||
|
|
||||||
const [date, setDate] = useState(selectedSlot?.date ?? selectedDate);
|
const [date, setDate] = useState(selectedSlot?.date ?? selectedDate);
|
||||||
const [sportId, setSportId] = useState(selectedSlot?.sportId ?? 'all');
|
const [sportId, setSportId] = useState(selectedSlot?.sportId ?? 'all');
|
||||||
const [courtId, setCourtId] = useState(selectedSlot?.courtId ?? '');
|
const [courtId, setCourtId] = useState(selectedSlot?.courtId ?? '');
|
||||||
const [startTime, setStartTime] = useState(selectedSlot?.startTime ?? '');
|
const [startTime, setStartTime] = useState(selectedSlot?.startTime ?? '');
|
||||||
|
const [isRecurring, setIsRecurring] = useState(false);
|
||||||
|
const [recurringEndDate, setRecurringEndDate] = useState<string | undefined>(undefined);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
@@ -76,6 +84,7 @@ export function BookingCreateDialog() {
|
|||||||
defaultValues: {
|
defaultValues: {
|
||||||
customerName: '',
|
customerName: '',
|
||||||
customerPhone: '',
|
customerPhone: '',
|
||||||
|
customerEmail: '',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -86,6 +95,8 @@ export function BookingCreateDialog() {
|
|||||||
setSportId(selectedSlot?.sportId ?? 'all');
|
setSportId(selectedSlot?.sportId ?? 'all');
|
||||||
setCourtId(selectedSlot?.courtId ?? '');
|
setCourtId(selectedSlot?.courtId ?? '');
|
||||||
setStartTime(selectedSlot?.startTime ?? '');
|
setStartTime(selectedSlot?.startTime ?? '');
|
||||||
|
setIsRecurring(false);
|
||||||
|
setRecurringEndDate(undefined);
|
||||||
reset();
|
reset();
|
||||||
|
|
||||||
if (selectedSlot?.courtId && selectedSlot?.startTime) {
|
if (selectedSlot?.courtId && selectedSlot?.startTime) {
|
||||||
@@ -126,6 +137,9 @@ export function BookingCreateDialog() {
|
|||||||
minute += selectedCourt.slotDurationMinutes
|
minute += selectedCourt.slotDurationMinutes
|
||||||
) {
|
) {
|
||||||
const slotEnd = minute + selectedCourt.slotDurationMinutes;
|
const slotEnd = minute + selectedCourt.slotDurationMinutes;
|
||||||
|
|
||||||
|
if (isTodayIso(date) && minute <= timeToMinutes(getNowTime())) continue;
|
||||||
|
|
||||||
const isBooked = courtBookings.some((booking) => {
|
const isBooked = courtBookings.some((booking) => {
|
||||||
const bookingStart = timeToMinutes(booking.startTime);
|
const bookingStart = timeToMinutes(booking.startTime);
|
||||||
const bookingEnd = timeToMinutes(booking.endTime);
|
const bookingEnd = timeToMinutes(booking.endTime);
|
||||||
@@ -148,13 +162,26 @@ export function BookingCreateDialog() {
|
|||||||
}, [availableStartTimes, startTime]);
|
}, [availableStartTimes, startTime]);
|
||||||
|
|
||||||
const onSubmit = async (values: BookingForm) => {
|
const onSubmit = async (values: BookingForm) => {
|
||||||
await createBooking({
|
if (isRecurring) {
|
||||||
courtId,
|
await createRecurringBooking({
|
||||||
date,
|
courtId,
|
||||||
startTime,
|
date,
|
||||||
customerName: values.customerName,
|
startTime,
|
||||||
customerPhone: values.customerPhone,
|
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 (
|
return (
|
||||||
@@ -163,17 +190,21 @@ export function BookingCreateDialog() {
|
|||||||
onOpenChange={(open) => !open && closeCreateBooking()}
|
onOpenChange={(open) => !open && closeCreateBooking()}
|
||||||
>
|
>
|
||||||
<ResponsiveDialogContent
|
<ResponsiveDialogContent
|
||||||
className="data-[variant=dialog]:max-w-lg"
|
className="group/create-booking data-[variant=dialog]:max-w-lg data-[variant=drawer]:!h-[92svh] data-[variant=drawer]:!max-h-[92svh] data-[variant=drawer]:overflow-hidden data-[variant=drawer]:px-0 data-[variant=drawer]:pb-0"
|
||||||
onOpenAutoFocus={(event) => event.preventDefault()}
|
onOpenAutoFocus={(event) => event.preventDefault()}
|
||||||
>
|
>
|
||||||
<ResponsiveDialogHeader>
|
<ResponsiveDialogHeader className="group-data-[variant=drawer]/create-booking:shrink-0 group-data-[variant=drawer]/create-booking:px-4 group-data-[variant=drawer]/create-booking:pb-3">
|
||||||
<ResponsiveDialogTitle>Nueva reserva</ResponsiveDialogTitle>
|
<ResponsiveDialogTitle>Nueva reserva</ResponsiveDialogTitle>
|
||||||
<ResponsiveDialogDescription>
|
<ResponsiveDialogDescription>
|
||||||
Crea un turno para atención telefónica o mostrador.
|
Crea un turno para atención telefónica o mostrador.
|
||||||
</ResponsiveDialogDescription>
|
</ResponsiveDialogDescription>
|
||||||
</ResponsiveDialogHeader>
|
</ResponsiveDialogHeader>
|
||||||
|
|
||||||
<form id="booking-create-form" className="grid gap-4" onSubmit={handleSubmit(onSubmit)}>
|
<form
|
||||||
|
id="booking-create-form"
|
||||||
|
className="grid gap-4 group-data-[variant=drawer]/create-booking:min-h-0 group-data-[variant=drawer]/create-booking:flex-1 group-data-[variant=drawer]/create-booking:overflow-y-auto group-data-[variant=drawer]/create-booking:px-4 group-data-[variant=drawer]/create-booking:pb-4"
|
||||||
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
|
>
|
||||||
<div className="grid gap-3 md:grid-cols-2">
|
<div className="grid gap-3 md:grid-cols-2">
|
||||||
<Field>
|
<Field>
|
||||||
<FieldLabel>Fecha</FieldLabel>
|
<FieldLabel>Fecha</FieldLabel>
|
||||||
@@ -248,6 +279,52 @@ export function BookingCreateDialog() {
|
|||||||
</Field>
|
</Field>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{isRecurringEnabled && (
|
||||||
|
<>
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<p className="text-sm font-medium">Repetir todas las semanas</p>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{isRecurring
|
||||||
|
? `Se creará una reserva todos los ${getDayOfWeekLabel(date)} a las ${startTime || '...'}`
|
||||||
|
: 'Crea turnos fijos en el mismo horario'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant={isRecurring ? 'default' : 'outline'}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setIsRecurring(!isRecurring)}
|
||||||
|
className="shrink-0"
|
||||||
|
>
|
||||||
|
{isRecurring ? 'Activado' : 'Desactivado'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isRecurring && (
|
||||||
|
<Field>
|
||||||
|
<FieldLabel>
|
||||||
|
Repetir hasta{' '}
|
||||||
|
<span className="text-muted-foreground font-normal">(opcional)</span>
|
||||||
|
</FieldLabel>
|
||||||
|
<DatePicker
|
||||||
|
value={fromIsoDateLocal(recurringEndDate ?? '')}
|
||||||
|
onChange={(nextDate) => {
|
||||||
|
setRecurringEndDate(nextDate ? toIsoDateLocal(nextDate) : undefined);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<p className="mt-1 text-xs text-muted-foreground">
|
||||||
|
Si no se selecciona una fecha, se repetirá por tiempo indefinido.
|
||||||
|
</p>
|
||||||
|
</Field>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
<Field data-invalid={Boolean(errors.customerName)}>
|
<Field data-invalid={Boolean(errors.customerName)}>
|
||||||
<FieldLabel htmlFor="customerName">Nombre</FieldLabel>
|
<FieldLabel htmlFor="customerName">Nombre</FieldLabel>
|
||||||
<Input
|
<Input
|
||||||
@@ -271,10 +348,27 @@ export function BookingCreateDialog() {
|
|||||||
<FieldError errors={[errors.customerPhone]} />
|
<FieldError errors={[errors.customerPhone]} />
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
|
<Field data-invalid={Boolean(errors.customerEmail)}>
|
||||||
|
<FieldLabel htmlFor="customerEmail">
|
||||||
|
Email <span className="text-muted-foreground font-normal">(opcional)</span>
|
||||||
|
</FieldLabel>
|
||||||
|
<Input
|
||||||
|
id="customerEmail"
|
||||||
|
type="email"
|
||||||
|
placeholder="Ej: juan@ejemplo.com"
|
||||||
|
aria-invalid={Boolean(errors.customerEmail)}
|
||||||
|
{...register('customerEmail')}
|
||||||
|
/>
|
||||||
|
<p className="mt-1 text-xs text-muted-foreground">
|
||||||
|
Se enviará la confirmación de la reserva por email.
|
||||||
|
</p>
|
||||||
|
<FieldError errors={[errors.customerEmail]} />
|
||||||
|
</Field>
|
||||||
|
|
||||||
{createBookingError && <p className="text-sm text-destructive">{createBookingError}</p>}
|
{createBookingError && <p className="text-sm text-destructive">{createBookingError}</p>}
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<ResponsiveDialogFooter>
|
<ResponsiveDialogFooter className="group-data-[variant=drawer]/create-booking:shrink-0 group-data-[variant=drawer]/create-booking:border-t group-data-[variant=drawer]/create-booking:border-border/70 group-data-[variant=drawer]/create-booking:bg-popover/95 group-data-[variant=drawer]/create-booking:px-4 group-data-[variant=drawer]/create-booking:pt-3 group-data-[variant=drawer]/create-booking:pb-[calc(12px+env(safe-area-inset-bottom))]">
|
||||||
<ResponsiveDialogClose asChild>
|
<ResponsiveDialogClose asChild>
|
||||||
<Button variant="outline">Cancelar</Button>
|
<Button variant="outline">Cancelar</Button>
|
||||||
</ResponsiveDialogClose>
|
</ResponsiveDialogClose>
|
||||||
@@ -283,10 +377,29 @@ export function BookingCreateDialog() {
|
|||||||
form="booking-create-form"
|
form="booking-create-form"
|
||||||
disabled={!isValid || isCreatingBooking || !courtId || !startTime}
|
disabled={!isValid || isCreatingBooking || !courtId || !startTime}
|
||||||
>
|
>
|
||||||
{isCreatingBooking ? 'Guardando...' : 'Crear reserva'}
|
{isCreatingBooking
|
||||||
|
? isRecurring
|
||||||
|
? 'Creando turnos...'
|
||||||
|
: 'Guardando...'
|
||||||
|
: isRecurring
|
||||||
|
? 'Crear turno fijo'
|
||||||
|
: 'Crear reserva'}
|
||||||
</Button>
|
</Button>
|
||||||
</ResponsiveDialogFooter>
|
</ResponsiveDialogFooter>
|
||||||
</ResponsiveDialogContent>
|
</ResponsiveDialogContent>
|
||||||
</ResponsiveDialog>
|
</ResponsiveDialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getDayOfWeekLabel(dateIso: string): string {
|
||||||
|
const labels: Record<string, string> = {
|
||||||
|
MONDAY: 'lunes',
|
||||||
|
TUESDAY: 'martes',
|
||||||
|
WEDNESDAY: 'miércoles',
|
||||||
|
THURSDAY: 'jueves',
|
||||||
|
FRIDAY: 'viernes',
|
||||||
|
SATURDAY: 'sábado',
|
||||||
|
SUNDAY: 'domingo',
|
||||||
|
};
|
||||||
|
return labels[getDayOfWeek(dateIso)] ?? '';
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { CalendarDays, Download, Drill, ShieldCheck, UsersRound } from 'lucide-react';
|
import { CalendarDays, Download, Repeat, ShieldCheck, UsersRound } from 'lucide-react';
|
||||||
import { useBooking } from '../booking-provider';
|
import { useBooking } from '../booking-provider';
|
||||||
import { formatBookingDate, toIsoDateLocal } from '../lib/booking-time';
|
import { formatBookingDate, toIsoDateLocal } from '../lib/booking-time';
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ export function BookingDaySummary() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-3 md:grid-cols-4">
|
<div className="grid gap-3 md:grid-cols-3">
|
||||||
<SummaryCard
|
<SummaryCard
|
||||||
label="Total de bloques"
|
label="Total de bloques"
|
||||||
value={summary.totalReservations}
|
value={summary.totalReservations}
|
||||||
@@ -35,13 +35,6 @@ export function BookingDaySummary() {
|
|||||||
icon={ShieldCheck}
|
icon={ShieldCheck}
|
||||||
className="border-reserved/35 bg-reserved/10 text-reserved"
|
className="border-reserved/35 bg-reserved/10 text-reserved"
|
||||||
/>
|
/>
|
||||||
<SummaryCard
|
|
||||||
label="Mantenimiento"
|
|
||||||
value={summary.maintenanceSlots}
|
|
||||||
detail={`${summary.maintenancePercent}% del total`}
|
|
||||||
icon={Drill}
|
|
||||||
className="border-maintenance/35 bg-maintenance/10 text-maintenance"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
@@ -73,11 +66,8 @@ function SummaryCard({ label, value, detail, icon: Icon, className }: SummaryCar
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function BookingQuickActions() {
|
export function BookingQuickActions() {
|
||||||
const { selectedDate, selectedStatus, setSelectedDate, setSelectedStatus, exportDayReport } =
|
const { setSelectedDate, setViewMode, exportDayReport, isRecurringEnabled } = useBooking();
|
||||||
useBooking();
|
|
||||||
const todayIso = toIsoDateLocal(new Date());
|
const todayIso = toIsoDateLocal(new Date());
|
||||||
const isViewingTodayReservations = selectedDate === todayIso && selectedStatus === 'reserved';
|
|
||||||
const isViewingMaintenance = selectedStatus === 'maintenance';
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="rounded-lg border bg-card/85 p-5 shadow-sm">
|
<section className="rounded-lg border bg-card/85 p-5 shadow-sm">
|
||||||
@@ -87,22 +77,12 @@ export function BookingQuickActions() {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedDate(todayIso);
|
setSelectedDate(todayIso);
|
||||||
setSelectedStatus(isViewingTodayReservations ? 'all' : 'reserved');
|
setViewMode('list');
|
||||||
}}
|
}}
|
||||||
aria-pressed={isViewingTodayReservations}
|
className="flex h-10 items-center gap-3 rounded-md border bg-background/45 px-3 text-left text-sm transition-colors hover:bg-muted"
|
||||||
className="flex h-10 items-center gap-3 rounded-md border bg-background/45 px-3 text-left text-sm transition-colors hover:bg-muted aria-pressed:border-reserved/50 aria-pressed:bg-reserved/10"
|
|
||||||
>
|
>
|
||||||
<CalendarDays className="size-4 text-muted-foreground" />
|
<CalendarDays className="size-4 text-muted-foreground" />
|
||||||
{isViewingTodayReservations ? 'Ver todos los estados' : 'Ver reservas de hoy'}
|
Ver reservas de hoy
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setSelectedStatus(isViewingMaintenance ? 'all' : 'maintenance')}
|
|
||||||
aria-pressed={isViewingMaintenance}
|
|
||||||
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-maintenance/50 aria-pressed:bg-maintenance/10"
|
|
||||||
>
|
|
||||||
<Drill className="size-4 text-maintenance" />
|
|
||||||
{isViewingMaintenance ? 'Ver todos los estados' : 'Ver mantenimiento'}
|
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -112,6 +92,16 @@ export function BookingQuickActions() {
|
|||||||
<Download className="size-4 text-primary" />
|
<Download className="size-4 text-primary" />
|
||||||
Exportar reporte
|
Exportar reporte
|
||||||
</button>
|
</button>
|
||||||
|
{isRecurringEnabled && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setViewMode('recurring')}
|
||||||
|
className="flex h-10 items-center gap-3 rounded-md border bg-background/45 px-3 text-left text-sm transition-colors hover:bg-muted"
|
||||||
|
>
|
||||||
|
<Repeat className="size-4 text-muted-foreground" />
|
||||||
|
Ver turnos fijos
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -31,7 +31,6 @@ import {
|
|||||||
Sun,
|
Sun,
|
||||||
User,
|
User,
|
||||||
Users,
|
Users,
|
||||||
Wrench,
|
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
@@ -352,18 +351,33 @@ function MobilePanelHeader() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function MobileFilters() {
|
function MobileFilters() {
|
||||||
const { selectedDate, setSelectedDate } = useBooking();
|
const { selectedDate, setSelectedDate, moveSelectedDate } = useBooking();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="grid grid-cols-[52px_1fr_52px] gap-3">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
className="h-12 rounded-lg"
|
||||||
|
onClick={() => moveSelectedDate(-1)}
|
||||||
|
>
|
||||||
|
<ChevronLeft className="size-5" />
|
||||||
|
</Button>
|
||||||
<DatePicker
|
<DatePicker
|
||||||
value={fromIsoDateLocal(selectedDate)}
|
value={fromIsoDateLocal(selectedDate)}
|
||||||
onChange={(date) =>
|
onChange={(date) =>
|
||||||
setSelectedDate(date ? toIsoDateLocal(date) : toIsoDateLocal(new Date()))
|
setSelectedDate(date ? toIsoDateLocal(date) : toIsoDateLocal(new Date()))
|
||||||
}
|
}
|
||||||
className="h-12 w-full justify-center rounded-lg border-border/70 bg-card/75 px-4 text-sm font-medium"
|
className="h-12 rounded-lg border-border/70 bg-card/75 text-sm"
|
||||||
placeholder="Fecha"
|
|
||||||
/>
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
className="h-12 rounded-lg"
|
||||||
|
onClick={() => moveSelectedDate(1)}
|
||||||
|
>
|
||||||
|
<ChevronRight className="size-5" />
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -385,11 +399,6 @@ function MobileSummary() {
|
|||||||
label="Reservados"
|
label="Reservados"
|
||||||
className="bg-reserved/15 text-reserved"
|
className="bg-reserved/15 text-reserved"
|
||||||
/>
|
/>
|
||||||
<SummaryPill
|
|
||||||
value={summary.maintenanceSlots}
|
|
||||||
label="Mantenim."
|
|
||||||
className="bg-maintenance/15 text-maintenance"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
@@ -411,7 +420,6 @@ function MobileStatusTab() {
|
|||||||
{ value: 'all', label: 'Todos' },
|
{ value: 'all', label: 'Todos' },
|
||||||
{ value: 'free', label: 'Libres' },
|
{ value: 'free', label: 'Libres' },
|
||||||
{ value: 'reserved', label: 'Reservados' },
|
{ value: 'reserved', label: 'Reservados' },
|
||||||
{ value: 'maintenance', label: 'Mantenimiento' },
|
|
||||||
] as const;
|
] as const;
|
||||||
const timeRangeOptions = [
|
const timeRangeOptions = [
|
||||||
{ label: '08:00 - 22:00', start: '08:00', end: '22:00' },
|
{ label: '08:00 - 22:00', start: '08:00', end: '22:00' },
|
||||||
@@ -835,14 +843,10 @@ function MobileSegmentRow({
|
|||||||
) : (
|
) : (
|
||||||
<div className="max-w-[132px] text-right">
|
<div className="max-w-[132px] text-right">
|
||||||
<p className="truncate text-sm font-medium">
|
<p className="truncate text-sm font-medium">
|
||||||
{segment.booking?.customerName ?? 'Mantenimiento'}
|
{segment.booking?.customerName ?? 'Sin información'}
|
||||||
</p>
|
</p>
|
||||||
<p className="mt-2 inline-flex items-center gap-1 text-sm text-muted-foreground">
|
<p className="mt-2 inline-flex items-center gap-1 text-sm text-muted-foreground">
|
||||||
{segment.status === 'maintenance' ? (
|
<Users className="size-4" />
|
||||||
<Wrench className="size-4" />
|
|
||||||
) : (
|
|
||||||
<Users className="size-4" />
|
|
||||||
)}
|
|
||||||
{segment.booking?.customerPhone ?? 'Personal'}
|
{segment.booking?.customerPhone ?? 'Personal'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -853,23 +857,21 @@ function MobileSegmentRow({
|
|||||||
|
|
||||||
function SegmentStatusLine({ segment }: { segment: BookingTimelineSegment }) {
|
function SegmentStatusLine({ segment }: { segment: BookingTimelineSegment }) {
|
||||||
const status = segment.status;
|
const status = segment.status;
|
||||||
const label = status === 'free' ? 'Libre' : status === 'reserved' ? 'Reservado' : 'Mantenimiento';
|
const label = status === 'free' ? 'Libre' : 'Reservado';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<p
|
<p
|
||||||
className={cn(
|
className={cn(
|
||||||
'mt-2 inline-flex items-center gap-2 text-sm',
|
'mt-2 inline-flex items-center gap-2 text-sm',
|
||||||
status === 'free' && 'text-primary',
|
status === 'free' && 'text-primary',
|
||||||
status === 'reserved' && 'text-reserved',
|
status === 'reserved' && 'text-reserved'
|
||||||
status === 'maintenance' && 'text-maintenance'
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
'size-3 rounded-full',
|
'size-3 rounded-full',
|
||||||
status === 'free' && 'bg-primary',
|
status === 'free' && 'bg-primary',
|
||||||
status === 'reserved' && 'bg-reserved',
|
status === 'reserved' && 'bg-reserved'
|
||||||
status === 'maintenance' && 'bg-maintenance'
|
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{label}
|
{label}
|
||||||
@@ -882,7 +884,6 @@ function StatusLegend() {
|
|||||||
<div className="flex items-center justify-between gap-3 text-sm">
|
<div className="flex items-center justify-between gap-3 text-sm">
|
||||||
<LegendItem label="Libre" className="bg-primary" />
|
<LegendItem label="Libre" className="bg-primary" />
|
||||||
<LegendItem label="Reservado" className="bg-reserved" />
|
<LegendItem label="Reservado" className="bg-reserved" />
|
||||||
<LegendItem label="Mantenimiento" className="bg-maintenance" />
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -914,9 +915,7 @@ function MiniSlot({
|
|||||||
className={cn(
|
className={cn(
|
||||||
'min-w-0 rounded-lg border px-1.5 py-2 text-center transition active:scale-[0.98] disabled:opacity-60',
|
'min-w-0 rounded-lg border px-1.5 py-2 text-center transition active:scale-[0.98] disabled:opacity-60',
|
||||||
segment.status === 'free' && 'border-primary/50 bg-primary/15 text-primary',
|
segment.status === 'free' && 'border-primary/50 bg-primary/15 text-primary',
|
||||||
segment.status === 'reserved' && 'border-reserved/50 bg-reserved/15 text-reserved',
|
segment.status === 'reserved' && 'border-reserved/50 bg-reserved/15 text-reserved'
|
||||||
segment.status === 'maintenance' &&
|
|
||||||
'border-maintenance/50 bg-maintenance/15 text-maintenance'
|
|
||||||
)}
|
)}
|
||||||
disabled={!isActionable}
|
disabled={!isActionable}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -966,10 +965,6 @@ function MetricDots({ schedule }: { schedule: BookingCourtSchedule }) {
|
|||||||
<span className="size-3 rounded-full bg-reserved" />
|
<span className="size-3 rounded-full bg-reserved" />
|
||||||
{schedule.metrics.reserved}
|
{schedule.metrics.reserved}
|
||||||
</span>
|
</span>
|
||||||
<span className="inline-flex items-center gap-1 text-maintenance">
|
|
||||||
<span className="size-3 rounded-full bg-maintenance" />
|
|
||||||
{schedule.metrics.maintenance}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
const items = [
|
const items = [
|
||||||
{ label: 'Libre', className: 'bg-primary' },
|
{ label: 'Libre', className: 'bg-primary' },
|
||||||
{ label: 'Reservado', className: 'bg-reserved' },
|
{ label: 'Reservado', className: 'bg-reserved' },
|
||||||
{ label: 'Mantenimiento', className: 'bg-maintenance' },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
export function BookingStatusLegend() {
|
export function BookingStatusLegend() {
|
||||||
|
|||||||
@@ -215,10 +215,6 @@ function BookingCourtInfo({ schedule }: { schedule: BookingCourtSchedule }) {
|
|||||||
<span className="size-2 rounded-full bg-reserved" />
|
<span className="size-2 rounded-full bg-reserved" />
|
||||||
{schedule.metrics.reserved}
|
{schedule.metrics.reserved}
|
||||||
</span>
|
</span>
|
||||||
<span className="inline-flex items-center gap-1">
|
|
||||||
<span className="size-2 rounded-full bg-maintenance" />
|
|
||||||
{schedule.metrics.maintenance}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -247,8 +243,7 @@ function BookingSlotBlock({ segment, schedule, rangeStart, totalMinutes }: Booki
|
|||||||
'border-primary/70 bg-primary/20 text-primary hover:bg-primary/25',
|
'border-primary/70 bg-primary/20 text-primary hover:bg-primary/25',
|
||||||
segment.status === 'reserved' &&
|
segment.status === 'reserved' &&
|
||||||
'border-reserved/70 bg-reserved/80 text-reserved-foreground',
|
'border-reserved/70 bg-reserved/80 text-reserved-foreground',
|
||||||
segment.status === 'maintenance' &&
|
|
||||||
'border-maintenance/80 bg-maintenance/75 text-maintenance-foreground',
|
|
||||||
segment.booking?.status === 'COMPLETED' &&
|
segment.booking?.status === 'COMPLETED' &&
|
||||||
'border-reserved/70 bg-reserved/25 dark:text-reserved-foreground text-gray-600 line-through',
|
'border-reserved/70 bg-reserved/25 dark:text-reserved-foreground text-gray-600 line-through',
|
||||||
segment.booking?.status === 'NOSHOW' &&
|
segment.booking?.status === 'NOSHOW' &&
|
||||||
@@ -300,7 +295,7 @@ function BookingSlotBlock({ segment, schedule, rangeStart, totalMinutes }: Booki
|
|||||||
</div>
|
</div>
|
||||||
<span className="mt-1 flex items-center gap-1 truncate opacity-90">
|
<span className="mt-1 flex items-center gap-1 truncate opacity-90">
|
||||||
<Users className="size-3" />
|
<Users className="size-3" />
|
||||||
{segment.booking?.customerName ?? 'Mantenimiento'}
|
{segment.booking?.customerName ?? 'Sin información'}
|
||||||
</span>
|
</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select';
|
} from '@/components/ui/select';
|
||||||
import { ChevronLeft, ChevronRight, Clock, SlidersHorizontal } from 'lucide-react';
|
import { ChevronLeft, ChevronRight, Clock } from 'lucide-react';
|
||||||
import { useBooking } from '../booking-provider';
|
import { useBooking } from '../booking-provider';
|
||||||
import type { BookingStatusFilter } from '../booking.types';
|
import type { BookingStatusFilter } from '../booking.types';
|
||||||
import { fromIsoDateLocal, toIsoDateLocal } from '../lib/booking-time';
|
import { fromIsoDateLocal, toIsoDateLocal } from '../lib/booking-time';
|
||||||
@@ -24,7 +24,6 @@ const statusOptions: Array<{ value: BookingStatusFilter; label: string }> = [
|
|||||||
{ value: 'all', label: 'Todos' },
|
{ value: 'all', label: 'Todos' },
|
||||||
{ value: 'free', label: 'Libre' },
|
{ value: 'free', label: 'Libre' },
|
||||||
{ value: 'reserved', label: 'Reservado' },
|
{ value: 'reserved', label: 'Reservado' },
|
||||||
{ value: 'maintenance', label: 'Mantenimiento' },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
export function BookingToolbar() {
|
export function BookingToolbar() {
|
||||||
@@ -46,7 +45,7 @@ export function BookingToolbar() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="rounded-lg border bg-card/85 p-4 shadow-sm">
|
<section className="rounded-lg border bg-card/85 p-4 shadow-sm">
|
||||||
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-[1fr_1fr_1.6fr_1.1fr_auto] xl:items-end">
|
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-[1fr_1fr_1.6fr_1.1fr] xl:items-end">
|
||||||
<Field>
|
<Field>
|
||||||
<FieldLabel>Deporte</FieldLabel>
|
<FieldLabel>Deporte</FieldLabel>
|
||||||
<Select value={selectedSportId} onValueChange={setSelectedSportId}>
|
<Select value={selectedSportId} onValueChange={setSelectedSportId}>
|
||||||
@@ -133,11 +132,6 @@ export function BookingToolbar() {
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
<Button variant="outline" className="xl:self-end">
|
|
||||||
<SlidersHorizontal className="size-4" />
|
|
||||||
Filtros avanzados
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import {
|
|||||||
Wrench,
|
Wrench,
|
||||||
XCircle,
|
XCircle,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
import { useState } from 'react';
|
||||||
import { useBooking } from '../booking-provider';
|
import { useBooking } from '../booking-provider';
|
||||||
|
|
||||||
function formatDate(date: string | undefined) {
|
function formatDate(date: string | undefined) {
|
||||||
@@ -70,6 +71,7 @@ export function BookingToolsDialog() {
|
|||||||
useBooking();
|
useBooking();
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const booking = selectedSegment?.booking;
|
const booking = selectedSegment?.booking;
|
||||||
|
const [confirmCancelOpen, setConfirmCancelOpen] = useState(false);
|
||||||
|
|
||||||
const status = statusConfig[booking?.status ?? 'CONFIRMED'] ?? {
|
const status = statusConfig[booking?.status ?? 'CONFIRMED'] ?? {
|
||||||
label: booking?.status,
|
label: booking?.status,
|
||||||
@@ -93,149 +95,211 @@ export function BookingToolsDialog() {
|
|||||||
closeBookingTools();
|
closeBookingTools();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const cancelConfirmDialog = (
|
||||||
|
<ResponsiveDialog
|
||||||
|
open={confirmCancelOpen}
|
||||||
|
onOpenChange={(open) => !open && setConfirmCancelOpen(false)}
|
||||||
|
>
|
||||||
|
<ResponsiveDialogContent>
|
||||||
|
<ResponsiveDialogHeader>
|
||||||
|
<ResponsiveDialogTitle>¿Cancelar reserva?</ResponsiveDialogTitle>
|
||||||
|
<ResponsiveDialogDescription>
|
||||||
|
Esta acción no se puede deshacer. Se eliminará la reserva y la cancha volverá a estar
|
||||||
|
disponible.
|
||||||
|
</ResponsiveDialogDescription>
|
||||||
|
</ResponsiveDialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-2 rounded-lg border bg-card p-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm text-muted-foreground">Cliente</span>
|
||||||
|
<span className="text-sm font-medium">{booking?.customerName ?? '-'}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm text-muted-foreground">Cancha</span>
|
||||||
|
<span className="text-sm font-medium">{booking?.courtName ?? '-'}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm text-muted-foreground">Fecha</span>
|
||||||
|
<span className="text-sm font-medium">{formatDate(booking?.date) || '-'}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm text-muted-foreground">Horario</span>
|
||||||
|
<span className="text-sm font-medium">
|
||||||
|
{booking?.startTime ?? '--:--'} a {booking?.endTime ?? '--:--'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ResponsiveDialogFooter>
|
||||||
|
<ResponsiveDialogClose asChild>
|
||||||
|
<Button variant="outline">Volver</Button>
|
||||||
|
</ResponsiveDialogClose>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
onClick={() => {
|
||||||
|
setConfirmCancelOpen(false);
|
||||||
|
updateStatus('CANCELLED');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Sí, cancelar reserva
|
||||||
|
</Button>
|
||||||
|
</ResponsiveDialogFooter>
|
||||||
|
</ResponsiveDialogContent>
|
||||||
|
</ResponsiveDialog>
|
||||||
|
);
|
||||||
|
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
<ResponsiveDialog
|
||||||
|
open={bookingToolsOpen}
|
||||||
|
onOpenChange={(open) => !open && closeBookingTools()}
|
||||||
|
>
|
||||||
|
<ResponsiveDialogContent
|
||||||
|
className="data-[variant=drawer]:!h-[92dvh] data-[variant=drawer]:!max-h-[92dvh] data-[variant=drawer]:overflow-hidden data-[variant=drawer]:px-0 data-[variant=drawer]:pb-0"
|
||||||
|
onOpenAutoFocus={(event) => event.preventDefault()}
|
||||||
|
>
|
||||||
|
<MobileBookingToolsSheet
|
||||||
|
status={status}
|
||||||
|
onComplete={() => updateStatus('COMPLETED')}
|
||||||
|
onCancel={() => setConfirmCancelOpen(true)}
|
||||||
|
onReminder={sendWhatsappReminder}
|
||||||
|
onNoShow={() => updateStatus('NOSHOW')}
|
||||||
|
/>
|
||||||
|
</ResponsiveDialogContent>
|
||||||
|
</ResponsiveDialog>
|
||||||
|
{cancelConfirmDialog}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
<ResponsiveDialog
|
<ResponsiveDialog
|
||||||
open={bookingToolsOpen}
|
open={bookingToolsOpen}
|
||||||
onOpenChange={(open) => !open && closeBookingTools()}
|
onOpenChange={(open) => !open && closeBookingTools()}
|
||||||
>
|
>
|
||||||
<ResponsiveDialogContent
|
<ResponsiveDialogContent
|
||||||
className="data-[variant=drawer]:!h-[92dvh] data-[variant=drawer]:!max-h-[92dvh] data-[variant=drawer]:overflow-hidden data-[variant=drawer]:px-0 data-[variant=drawer]:pb-0"
|
className="data-[variant=dialog]:max-w-5xl data-[variant=drawer]:max-h-[92dvh] data-[variant=drawer]:overflow-y-auto data-[variant=drawer]:px-4 data-[variant=drawer]:pb-5"
|
||||||
onOpenAutoFocus={(event) => event.preventDefault()}
|
onOpenAutoFocus={(event) => event.preventDefault()}
|
||||||
>
|
>
|
||||||
<MobileBookingToolsSheet
|
<ResponsiveDialogHeader className="data-[variant=drawer]:px-0 data-[variant=drawer]:text-left">
|
||||||
status={status}
|
<ResponsiveDialogTitle>Detalles de la reserva</ResponsiveDialogTitle>
|
||||||
onComplete={() => updateStatus('COMPLETED')}
|
<ResponsiveDialogDescription>
|
||||||
onCancel={() => updateStatus('CANCELLED')}
|
Administra la reserva y su estado.
|
||||||
onReminder={sendWhatsappReminder}
|
</ResponsiveDialogDescription>
|
||||||
onNoShow={() => updateStatus('NOSHOW')}
|
</ResponsiveDialogHeader>
|
||||||
/>
|
|
||||||
|
<section className="mx-0 px-0 pt-3 pb-3 sm:-mx-6 sm:px-6 sm:pb-5">
|
||||||
|
<div className="grid grid-cols-1 gap-3 sm:grid-cols-3 sm:gap-0">
|
||||||
|
<DetailItem
|
||||||
|
icon={<MapPin className="h-5 w-5" />}
|
||||||
|
label="Cancha"
|
||||||
|
value={selectedSegment?.booking?.courtName ?? '-'}
|
||||||
|
helper={selectedSegment?.booking?.sport?.name}
|
||||||
|
withDivider
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DetailItem
|
||||||
|
icon={<BadgeCheck className="h-5 w-5" />}
|
||||||
|
label="Estado actual"
|
||||||
|
value={status.label}
|
||||||
|
valueClassName="text-emerald-400 uppercase dark:text-emerald-500"
|
||||||
|
withDivider
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DetailItem
|
||||||
|
icon={<User className="h-5 w-5" />}
|
||||||
|
label="Cliente"
|
||||||
|
value={selectedSegment?.booking?.customerName ?? '-'}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DetailItem
|
||||||
|
icon={<Clock className="h-5 w-5" />}
|
||||||
|
label="Fecha"
|
||||||
|
value={`${formatDate(selectedSegment?.booking?.date)}`}
|
||||||
|
withDivider
|
||||||
|
className="sm:pt-10"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DetailItem
|
||||||
|
icon={<Clock className="h-5 w-5" />}
|
||||||
|
label="Hora"
|
||||||
|
value={`${selectedSegment?.booking?.startTime} a ${selectedSegment?.booking?.endTime}`}
|
||||||
|
withDivider
|
||||||
|
className="sm:pt-10"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DetailItem
|
||||||
|
icon={<Phone className="h-5 w-5" />}
|
||||||
|
label="Teléfono"
|
||||||
|
value={selectedSegment?.booking?.customerPhone ?? '-'}
|
||||||
|
className="sm:pt-10"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<Separator className="dark:bg-slate-800 bg-slate-300" />
|
||||||
|
|
||||||
|
{booking?.status !== 'COMPLETED' && (
|
||||||
|
<section className="space-y-3">
|
||||||
|
<div>
|
||||||
|
<h3 className="font-medium text-slate-100">Acciones disponibles</h3>
|
||||||
|
<p className="text-sm text-slate-400">
|
||||||
|
Elegí qué hacer según lo que ocurrió con el cliente.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-3 sm:grid-cols-2">
|
||||||
|
<ActionButton
|
||||||
|
icon={<CheckCircle2 className="h-5 w-5" />}
|
||||||
|
title="Completar reserva"
|
||||||
|
description="El cliente se presentó para usar la cancha."
|
||||||
|
className="border-emerald-500/40 bg-emerald-500/10 text-lg text-emerald-600 hover:bg-emerald-500/15 hover:text-emerald-800"
|
||||||
|
onClick={() => {
|
||||||
|
updateStatus('COMPLETED');
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ActionButton
|
||||||
|
icon={<XCircle className="h-5 w-5" />}
|
||||||
|
title="Cancelar reserva"
|
||||||
|
description="El cliente avisó que no va a venir. La cancha vuelve a estar disponible."
|
||||||
|
className="border-red-500/40 bg-red-500/10 text-lg text-red-600 hover:bg-red-500/15 hover:text-red-800"
|
||||||
|
onClick={() => setConfirmCancelOpen(true)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ActionButton
|
||||||
|
icon={<MessageCircle className="h-5 w-5" />}
|
||||||
|
title="Enviar recordatorio"
|
||||||
|
description="Enviar un mensaje por WhatsApp al cliente."
|
||||||
|
className="border-sky-500/40 bg-sky-500/10 text-lg text-sky-600 hover:bg-sky-500/15 hover:text-sky-800"
|
||||||
|
onClick={sendWhatsappReminder}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ActionButton
|
||||||
|
icon={<AlertTriangle className="h-5 w-5" />}
|
||||||
|
title="Marcar como No Show"
|
||||||
|
description="El cliente no vino y no canceló."
|
||||||
|
className="border-amber-500/40 bg-amber-500/10 text-lg text-amber-600 hover:bg-amber-500/15 hover:text-amber-800"
|
||||||
|
onClick={() => {
|
||||||
|
updateStatus('NOSHOW');
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<ResponsiveDialogFooter className="data-[variant=drawer]:px-0">
|
||||||
|
<ResponsiveDialogClose asChild>
|
||||||
|
<Button variant="outline" className="data-[variant=drawer]:h-11">
|
||||||
|
Cerrar
|
||||||
|
</Button>
|
||||||
|
</ResponsiveDialogClose>
|
||||||
|
</ResponsiveDialogFooter>
|
||||||
</ResponsiveDialogContent>
|
</ResponsiveDialogContent>
|
||||||
</ResponsiveDialog>
|
</ResponsiveDialog>
|
||||||
);
|
{cancelConfirmDialog}
|
||||||
}
|
</>
|
||||||
|
|
||||||
return (
|
|
||||||
<ResponsiveDialog open={bookingToolsOpen} onOpenChange={(open) => !open && closeBookingTools()}>
|
|
||||||
<ResponsiveDialogContent
|
|
||||||
className="data-[variant=dialog]:max-w-5xl data-[variant=drawer]:max-h-[92dvh] data-[variant=drawer]:overflow-y-auto data-[variant=drawer]:px-4 data-[variant=drawer]:pb-5"
|
|
||||||
onOpenAutoFocus={(event) => event.preventDefault()}
|
|
||||||
>
|
|
||||||
<ResponsiveDialogHeader className="data-[variant=drawer]:px-0 data-[variant=drawer]:text-left">
|
|
||||||
<ResponsiveDialogTitle>Detalles de la reserva</ResponsiveDialogTitle>
|
|
||||||
<ResponsiveDialogDescription>
|
|
||||||
Administra la reserva y su estado.
|
|
||||||
</ResponsiveDialogDescription>
|
|
||||||
</ResponsiveDialogHeader>
|
|
||||||
|
|
||||||
<section className="mx-0 px-0 pt-3 pb-3 sm:-mx-6 sm:px-6 sm:pb-5">
|
|
||||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-3 sm:gap-0">
|
|
||||||
<DetailItem
|
|
||||||
icon={<MapPin className="h-5 w-5" />}
|
|
||||||
label="Cancha"
|
|
||||||
value={selectedSegment?.booking?.courtName ?? '-'}
|
|
||||||
helper={selectedSegment?.booking?.sport?.name}
|
|
||||||
withDivider
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DetailItem
|
|
||||||
icon={<BadgeCheck className="h-5 w-5" />}
|
|
||||||
label="Estado actual"
|
|
||||||
value={status.label}
|
|
||||||
valueClassName="text-emerald-400 uppercase dark:text-emerald-500"
|
|
||||||
withDivider
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DetailItem
|
|
||||||
icon={<User className="h-5 w-5" />}
|
|
||||||
label="Cliente"
|
|
||||||
value={selectedSegment?.booking?.customerName ?? '-'}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DetailItem
|
|
||||||
icon={<Clock className="h-5 w-5" />}
|
|
||||||
label="Fecha"
|
|
||||||
value={`${formatDate(selectedSegment?.booking?.date)}`}
|
|
||||||
withDivider
|
|
||||||
className="sm:pt-10"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DetailItem
|
|
||||||
icon={<Clock className="h-5 w-5" />}
|
|
||||||
label="Hora"
|
|
||||||
value={`${selectedSegment?.booking?.startTime} a ${selectedSegment?.booking?.endTime}`}
|
|
||||||
withDivider
|
|
||||||
className="sm:pt-10"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DetailItem
|
|
||||||
icon={<Phone className="h-5 w-5" />}
|
|
||||||
label="Teléfono"
|
|
||||||
value={selectedSegment?.booking?.customerPhone ?? '-'}
|
|
||||||
className="sm:pt-10"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<Separator className="dark:bg-slate-800 bg-slate-300" />
|
|
||||||
|
|
||||||
<section className="space-y-3">
|
|
||||||
<div>
|
|
||||||
<h3 className="font-medium text-slate-100">Acciones disponibles</h3>
|
|
||||||
<p className="text-sm text-slate-400">
|
|
||||||
Elegí qué hacer según lo que ocurrió con el cliente.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="grid gap-3 sm:grid-cols-2">
|
|
||||||
<ActionButton
|
|
||||||
icon={<CheckCircle2 className="h-5 w-5" />}
|
|
||||||
title="Completar reserva"
|
|
||||||
description="El cliente se presentó para usar la cancha."
|
|
||||||
className="border-emerald-500/40 bg-emerald-500/10 text-lg text-emerald-600 hover:bg-emerald-500/15 hover:text-emerald-800"
|
|
||||||
onClick={() => {
|
|
||||||
updateStatus('COMPLETED');
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ActionButton
|
|
||||||
icon={<XCircle className="h-5 w-5" />}
|
|
||||||
title="Cancelar reserva"
|
|
||||||
description="El cliente avisó que no va a venir. La cancha vuelve a estar disponible."
|
|
||||||
className="border-red-500/40 bg-red-500/10 text-lg text-red-600 hover:bg-red-500/15 hover:text-red-800"
|
|
||||||
onClick={() => {
|
|
||||||
updateStatus('CANCELLED');
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ActionButton
|
|
||||||
icon={<MessageCircle className="h-5 w-5" />}
|
|
||||||
title="Enviar recordatorio"
|
|
||||||
description="Enviar un mensaje por WhatsApp al cliente."
|
|
||||||
className="border-sky-500/40 bg-sky-500/10 text-lg text-sky-600 hover:bg-sky-500/15 hover:text-sky-800"
|
|
||||||
onClick={sendWhatsappReminder}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ActionButton
|
|
||||||
icon={<AlertTriangle className="h-5 w-5" />}
|
|
||||||
title="Marcar como No Show"
|
|
||||||
description="El cliente no vino y no canceló."
|
|
||||||
className="border-amber-500/40 bg-amber-500/10 text-lg text-amber-600 hover:bg-amber-500/15 hover:text-amber-800"
|
|
||||||
onClick={() => {
|
|
||||||
updateStatus('NOSHOW');
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<ResponsiveDialogFooter className="data-[variant=drawer]:px-0">
|
|
||||||
<ResponsiveDialogClose asChild>
|
|
||||||
<Button variant="outline" className="data-[variant=drawer]:h-11">
|
|
||||||
Cerrar
|
|
||||||
</Button>
|
|
||||||
</ResponsiveDialogClose>
|
|
||||||
</ResponsiveDialogFooter>
|
|
||||||
</ResponsiveDialogContent>
|
|
||||||
</ResponsiveDialog>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -321,42 +385,44 @@ function MobileBookingToolsSheet({
|
|||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section className="shrink-0 border-t border-border/70 bg-popover/95 px-4 py-3">
|
{booking?.status !== 'COMPLETED' && (
|
||||||
<h3 className="text-sm font-medium">Acciones</h3>
|
<section className="shrink-0 border-t border-border/70 bg-popover/95 px-4 py-3">
|
||||||
<div className="mt-3 grid grid-cols-2 gap-2">
|
<h3 className="text-sm font-medium">Acciones</h3>
|
||||||
<MobileActionButton
|
<div className="mt-3 grid grid-cols-2 gap-2">
|
||||||
icon={<CheckCircle2 className="size-5" />}
|
<MobileActionButton
|
||||||
label="Completar"
|
icon={<CheckCircle2 className="size-5" />}
|
||||||
className="border-primary/45 bg-primary/12 text-primary"
|
label="Completar"
|
||||||
onClick={onComplete}
|
className="border-primary/45 bg-primary/12 text-primary"
|
||||||
/>
|
onClick={onComplete}
|
||||||
<MobileActionButton
|
/>
|
||||||
icon={<MessageCircle className="size-5" />}
|
<MobileActionButton
|
||||||
label="Recordar"
|
icon={<MessageCircle className="size-5" />}
|
||||||
className="border-reserved/45 bg-reserved/12 text-reserved"
|
label="Recordar"
|
||||||
onClick={onReminder}
|
className="border-reserved/45 bg-reserved/12 text-reserved"
|
||||||
/>
|
onClick={onReminder}
|
||||||
<MobileActionButton
|
/>
|
||||||
icon={<XCircle className="size-5" />}
|
<MobileActionButton
|
||||||
label="Cancelar"
|
icon={<XCircle className="size-5" />}
|
||||||
className="border-destructive/45 bg-destructive/12 text-destructive"
|
label="Cancelar"
|
||||||
onClick={onCancel}
|
className="border-destructive/45 bg-destructive/12 text-destructive"
|
||||||
/>
|
onClick={onCancel}
|
||||||
<MobileActionButton
|
/>
|
||||||
icon={<AlertTriangle className="size-5" />}
|
<MobileActionButton
|
||||||
label="No show"
|
icon={<AlertTriangle className="size-5" />}
|
||||||
className="border-warning/50 bg-warning/12 text-warning"
|
label="No show"
|
||||||
onClick={onNoShow}
|
className="border-warning/50 bg-warning/12 text-warning"
|
||||||
/>
|
onClick={onNoShow}
|
||||||
</div>
|
/>
|
||||||
<ResponsiveDialogFooter className="px-0 pb-0 pt-3">
|
</div>
|
||||||
<ResponsiveDialogClose asChild>
|
<ResponsiveDialogFooter className="px-0 pb-0 pt-3">
|
||||||
<Button variant="outline" className="h-11 w-full rounded-lg">
|
<ResponsiveDialogClose asChild>
|
||||||
Cerrar
|
<Button variant="outline" className="h-11 w-full rounded-lg">
|
||||||
</Button>
|
Cerrar
|
||||||
</ResponsiveDialogClose>
|
</Button>
|
||||||
</ResponsiveDialogFooter>
|
</ResponsiveDialogClose>
|
||||||
</section>
|
</ResponsiveDialogFooter>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,210 @@
|
|||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/select';
|
||||||
|
import { apiClient } from '@/lib/api-client';
|
||||||
|
import type { RecurringBookingGroup } from '@repo/api-contract';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useBooking } from '../booking-provider';
|
||||||
|
import { minutesToTime, timeToMinutes } from '../lib/booking-time';
|
||||||
|
|
||||||
|
const DAY_OPTIONS = [
|
||||||
|
{ value: 'MONDAY', label: 'Lunes' },
|
||||||
|
{ value: 'TUESDAY', label: 'Martes' },
|
||||||
|
{ value: 'WEDNESDAY', label: 'Miércoles' },
|
||||||
|
{ value: 'THURSDAY', label: 'Jueves' },
|
||||||
|
{ value: 'FRIDAY', label: 'Viernes' },
|
||||||
|
{ value: 'SATURDAY', label: 'Sábado' },
|
||||||
|
{ value: 'SUNDAY', label: 'Domingo' },
|
||||||
|
];
|
||||||
|
|
||||||
|
interface RecurringBookingEditDialogProps {
|
||||||
|
group: RecurringBookingGroup;
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RecurringBookingEditDialog({
|
||||||
|
group,
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
}: RecurringBookingEditDialogProps) {
|
||||||
|
const { courts, refreshRecurringGroups } = useBooking();
|
||||||
|
|
||||||
|
const [courtId, setCourtId] = useState<string>(group.courtId);
|
||||||
|
const [dayOfWeek, setDayOfWeek] = useState<string>(group.dayOfWeek);
|
||||||
|
const [startTime, setStartTime] = useState<string>(group.startTime);
|
||||||
|
const [customerName, setCustomerName] = useState(group.customerName);
|
||||||
|
const [customerPhone, setCustomerPhone] = useState(group.customerPhone);
|
||||||
|
const [customerEmail, setCustomerEmail] = useState(group.customerEmail);
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setCourtId(group.courtId);
|
||||||
|
setDayOfWeek(group.dayOfWeek);
|
||||||
|
setStartTime(group.startTime);
|
||||||
|
setCustomerName(group.customerName);
|
||||||
|
setCustomerPhone(group.customerPhone);
|
||||||
|
setCustomerEmail(group.customerEmail);
|
||||||
|
}, [group]);
|
||||||
|
|
||||||
|
const selectedCourt = courts.find((c) => c.id === courtId);
|
||||||
|
const slotDuration = selectedCourt?.slotDurationMinutes ?? 60;
|
||||||
|
|
||||||
|
const availableSlots: string[] = [];
|
||||||
|
if (selectedCourt) {
|
||||||
|
const availabilities = selectedCourt.availability.filter((a) => a.dayOfWeek === dayOfWeek);
|
||||||
|
for (const avail of availabilities) {
|
||||||
|
const startMins = timeToMinutes(avail.startTime);
|
||||||
|
const endMins = timeToMinutes(avail.endTime);
|
||||||
|
for (let m = startMins; m + slotDuration <= endMins; m += slotDuration) {
|
||||||
|
availableSlots.push(minutesToTime(m));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
if (!customerName || customerName.trim().length < 2) {
|
||||||
|
setError('El nombre debe tener al menos 2 caracteres.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setError('');
|
||||||
|
setSaving(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await apiClient.adminBookings.updateRecurringGroup(group.id, {
|
||||||
|
...(courtId !== group.courtId && { courtId }),
|
||||||
|
...(dayOfWeek !== group.dayOfWeek && {
|
||||||
|
dayOfWeek: dayOfWeek as
|
||||||
|
| 'MONDAY'
|
||||||
|
| 'TUESDAY'
|
||||||
|
| 'WEDNESDAY'
|
||||||
|
| 'THURSDAY'
|
||||||
|
| 'FRIDAY'
|
||||||
|
| 'SATURDAY'
|
||||||
|
| 'SUNDAY',
|
||||||
|
}),
|
||||||
|
...(startTime !== group.startTime && { startTime }),
|
||||||
|
...(customerName !== group.customerName && { customerName }),
|
||||||
|
...(customerPhone !== group.customerPhone && { customerPhone }),
|
||||||
|
...(customerEmail !== group.customerEmail && { customerEmail }),
|
||||||
|
});
|
||||||
|
|
||||||
|
await refreshRecurringGroups();
|
||||||
|
onClose();
|
||||||
|
} catch (e) {
|
||||||
|
setError(e instanceof Error ? e.message : 'Error al guardar los cambios.');
|
||||||
|
} finally {
|
||||||
|
setSaving(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={(open) => !open && onClose()}>
|
||||||
|
<DialogContent className="sm:max-w-md">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Editar turno fijo</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label>Cliente</Label>
|
||||||
|
<Input
|
||||||
|
value={customerName}
|
||||||
|
onChange={(e) => setCustomerName(e.target.value)}
|
||||||
|
placeholder="Nombre del cliente"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label>Teléfono</Label>
|
||||||
|
<Input
|
||||||
|
value={customerPhone}
|
||||||
|
onChange={(e) => setCustomerPhone(e.target.value)}
|
||||||
|
placeholder="Teléfono"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label>Email</Label>
|
||||||
|
<Input
|
||||||
|
value={customerEmail}
|
||||||
|
onChange={(e) => setCustomerEmail(e.target.value)}
|
||||||
|
placeholder="Email"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label>Cancha</Label>
|
||||||
|
<Select value={courtId} onValueChange={setCourtId}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Cancha" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{courts.map((court) => (
|
||||||
|
<SelectItem key={court.id} value={court.id}>
|
||||||
|
{court.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label>Día</Label>
|
||||||
|
<Select value={dayOfWeek} onValueChange={setDayOfWeek}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Día" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{DAY_OPTIONS.map((day) => (
|
||||||
|
<SelectItem key={day.value} value={day.value}>
|
||||||
|
{day.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label>Horario</Label>
|
||||||
|
<Select value={startTime} onValueChange={setStartTime}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Horario" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{availableSlots.map((slot) => (
|
||||||
|
<SelectItem key={slot} value={slot}>
|
||||||
|
{slot} - {minutesToTime(timeToMinutes(slot) + slotDuration)}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && <p className="text-sm text-destructive">{error}</p>}
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-3 pt-2">
|
||||||
|
<Button type="button" variant="outline" onClick={onClose}>
|
||||||
|
Cancelar
|
||||||
|
</Button>
|
||||||
|
<Button type="button" onClick={handleSave} disabled={saving}>
|
||||||
|
{saving ? 'Guardando...' : 'Guardar cambios'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,246 @@
|
|||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
|
import type { RecurringBookingGroup } from '@repo/api-contract';
|
||||||
|
import { ArrowLeft, Clock, MapPin, Pencil, Repeat, Trash2, User } from 'lucide-react';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useBooking } from '../booking-provider';
|
||||||
|
import { RecurringBookingEditDialog } from './recurring-booking-edit-dialog';
|
||||||
|
|
||||||
|
const DAY_LABELS: Record<string, string> = {
|
||||||
|
MONDAY: 'Lunes',
|
||||||
|
TUESDAY: 'Martes',
|
||||||
|
WEDNESDAY: 'Miércoles',
|
||||||
|
THURSDAY: 'Jueves',
|
||||||
|
FRIDAY: 'Viernes',
|
||||||
|
SATURDAY: 'Sábado',
|
||||||
|
SUNDAY: 'Domingo',
|
||||||
|
};
|
||||||
|
|
||||||
|
function getNextBookingDate(group: RecurringBookingGroup): string {
|
||||||
|
const today = new Date().toISOString().slice(0, 10);
|
||||||
|
const future = group.bookings
|
||||||
|
.filter((b) => b.date >= today && b.status === 'CONFIRMED')
|
||||||
|
.sort((a, b) => a.date.localeCompare(b.date));
|
||||||
|
return future[0]?.date ?? '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(dateStr: string): string {
|
||||||
|
if (dateStr === '-') return '-';
|
||||||
|
const [y, m, d] = dateStr.split('-');
|
||||||
|
return `${d}/${m}/${y}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RecurringBookingListView() {
|
||||||
|
const {
|
||||||
|
setViewMode,
|
||||||
|
recurringGroups,
|
||||||
|
isRecurringGroupsLoading,
|
||||||
|
cancelRecurringGroup,
|
||||||
|
isCancellingRecurringGroup,
|
||||||
|
} = useBooking();
|
||||||
|
|
||||||
|
const [editingGroup, setEditingGroup] = useState<RecurringBookingGroup | null>(null);
|
||||||
|
const [cancellingGroupId, setCancellingGroupId] = useState<string | null>(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<section className="rounded-lg border bg-card/85 shadow-sm">
|
||||||
|
<div className="flex flex-wrap items-center justify-between gap-4 border-b px-4 py-4">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="rounded-full"
|
||||||
|
onClick={() => setViewMode('panel')}
|
||||||
|
>
|
||||||
|
<ArrowLeft className="size-5" />
|
||||||
|
</Button>
|
||||||
|
<div>
|
||||||
|
<h2 className="text-base font-semibold">Turnos fijos</h2>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{recurringGroups.length}{' '}
|
||||||
|
{recurringGroups.length === 1 ? 'turno fijo' : 'turnos fijos'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isRecurringGroupsLoading && (
|
||||||
|
<div className="px-4 py-10 text-sm text-muted-foreground">Cargando turnos fijos...</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isRecurringGroupsLoading && recurringGroups.length === 0 && (
|
||||||
|
<div className="px-4 py-10 text-center text-sm text-muted-foreground">
|
||||||
|
<Repeat className="mx-auto mb-3 size-10 opacity-40" />
|
||||||
|
<p>No hay turnos fijos creados.</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isRecurringGroupsLoading && recurringGroups.length > 0 && (
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="hidden w-full sm:table">
|
||||||
|
<thead>
|
||||||
|
<tr className="border-b text-left text-xs font-medium uppercase tracking-wider text-muted-foreground">
|
||||||
|
<th className="px-4 py-3 font-medium">Cliente</th>
|
||||||
|
<th className="px-4 py-3 font-medium">Cancha</th>
|
||||||
|
<th className="px-4 py-3 font-medium">Día</th>
|
||||||
|
<th className="px-4 py-3 font-medium">Horario</th>
|
||||||
|
<th className="px-4 py-3 font-medium">Próxima fecha</th>
|
||||||
|
<th className="px-4 py-3 font-medium">Acciones</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y">
|
||||||
|
{recurringGroups.map((group) => {
|
||||||
|
const nextDate = getNextBookingDate(group);
|
||||||
|
return (
|
||||||
|
<tr key={group.id} className="transition-colors hover:bg-muted/50">
|
||||||
|
<td className="px-4 py-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<User className="size-4 shrink-0 text-muted-foreground" />
|
||||||
|
<span className="truncate text-sm">{group.customerName || '-'}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<MapPin className="size-4 shrink-0 text-muted-foreground" />
|
||||||
|
<span className="truncate text-sm">
|
||||||
|
{group.bookings[0]?.courtName ?? '-'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-4 text-sm">
|
||||||
|
{DAY_LABELS[group.dayOfWeek] ?? group.dayOfWeek}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Clock className="size-4 shrink-0 text-muted-foreground" />
|
||||||
|
<span className="text-sm">
|
||||||
|
{group.startTime} - {group.endTime}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-4 text-sm">{formatDate(nextDate)}</td>
|
||||||
|
<td className="px-4 py-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setEditingGroup(group)}
|
||||||
|
className="flex items-center gap-1.5 rounded-md border px-2.5 py-1.5 text-xs font-medium transition-colors hover:bg-muted"
|
||||||
|
>
|
||||||
|
<Pencil className="size-3.5" />
|
||||||
|
Editar
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setCancellingGroupId(group.id)}
|
||||||
|
disabled={isCancellingRecurringGroup}
|
||||||
|
className="flex items-center gap-1.5 rounded-md border border-red-200 px-2.5 py-1.5 text-xs font-medium text-red-600 transition-colors hover:bg-red-50 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
<Trash2 className="size-3.5" />
|
||||||
|
Cancelar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div className="divide-y sm:hidden">
|
||||||
|
{recurringGroups.map((group) => {
|
||||||
|
const nextDate = getNextBookingDate(group);
|
||||||
|
return (
|
||||||
|
<div key={group.id} className="px-4 py-4">
|
||||||
|
<div className="mb-2 flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<User className="size-4 text-muted-foreground" />
|
||||||
|
<span className="text-sm font-semibold">{group.customerName || '-'}</span>
|
||||||
|
</div>
|
||||||
|
<span className="rounded-full bg-primary/10 px-2 py-0.5 text-xs font-medium text-primary">
|
||||||
|
{DAY_LABELS[group.dayOfWeek]}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{group.bookings[0]?.courtName} · {group.startTime} - {group.endTime}
|
||||||
|
</p>
|
||||||
|
<p className="mt-1 text-xs text-muted-foreground">
|
||||||
|
Próxima: {formatDate(nextDate)}
|
||||||
|
</p>
|
||||||
|
<div className="mt-2 flex gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setEditingGroup(group)}
|
||||||
|
className="flex items-center gap-1 rounded-md border px-2 py-1 text-xs transition-colors hover:bg-muted"
|
||||||
|
>
|
||||||
|
<Pencil className="size-3" />
|
||||||
|
Editar
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setCancellingGroupId(group.id)}
|
||||||
|
disabled={isCancellingRecurringGroup}
|
||||||
|
className="flex items-center gap-1 rounded-md border border-red-200 px-2 py-1 text-xs text-red-600 transition-colors hover:bg-red-50 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
<Trash2 className="size-3" />
|
||||||
|
Cancelar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<Dialog
|
||||||
|
open={!!cancellingGroupId}
|
||||||
|
onOpenChange={(open) => !open && setCancellingGroupId(null)}
|
||||||
|
>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Cancelar turno fijo</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
¿Estás seguro de cancelar este turno fijo? Se eliminarán todas las reservas futuras.
|
||||||
|
Las reservas pasadas no se verán afectadas.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => setCancellingGroupId(null)}>
|
||||||
|
Volver
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
disabled={isCancellingRecurringGroup}
|
||||||
|
onClick={() => {
|
||||||
|
if (cancellingGroupId) {
|
||||||
|
cancelRecurringGroup(cancellingGroupId);
|
||||||
|
setCancellingGroupId(null);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isCancellingRecurringGroup ? 'Cancelando...' : 'Sí, cancelar turno fijo'}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
{editingGroup && (
|
||||||
|
<RecurringBookingEditDialog
|
||||||
|
group={editingGroup}
|
||||||
|
open={!!editingGroup}
|
||||||
|
onClose={() => setEditingGroup(null)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -49,8 +49,14 @@ export function HomePage() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function autoSelect() {
|
async function autoSelect() {
|
||||||
if (myComplexesQuery.data && myComplexesQuery.data.length === 1) {
|
if (myComplexesQuery.data && myComplexesQuery.data.length === 1) {
|
||||||
await apiClient.complexes.select({ complexId: myComplexesQuery.data[0].id });
|
try {
|
||||||
queryClient.invalidateQueries({ queryKey: ['current-complex'] });
|
const result = await apiClient.complexes.select({
|
||||||
|
complexId: myComplexesQuery.data[0].id,
|
||||||
|
});
|
||||||
|
queryClient.setQueryData(['current-complex'], result);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error al auto-seleccionar complejo:', error);
|
||||||
|
}
|
||||||
} else if (
|
} else if (
|
||||||
myComplexesQuery.data &&
|
myComplexesQuery.data &&
|
||||||
myComplexesQuery.data.length > 1 &&
|
myComplexesQuery.data.length > 1 &&
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ const FEATURE_LABELS: Record<string, string> = {
|
|||||||
onlinePayments: 'Pagos online',
|
onlinePayments: 'Pagos online',
|
||||||
advancedReports: 'Reportes avanzados',
|
advancedReports: 'Reportes avanzados',
|
||||||
whatsappReminders: 'Recordatorios WhatsApp',
|
whatsappReminders: 'Recordatorios WhatsApp',
|
||||||
|
fixedSlots: 'Turnos fijos',
|
||||||
};
|
};
|
||||||
|
|
||||||
export function PlanSelectStep({ form, plans }: PlanSelectStepProps) {
|
export function PlanSelectStep({ form, plans }: PlanSelectStepProps) {
|
||||||
@@ -24,6 +25,7 @@ export function PlanSelectStep({ form, plans }: PlanSelectStepProps) {
|
|||||||
'onlinePayments',
|
'onlinePayments',
|
||||||
'advancedReports',
|
'advancedReports',
|
||||||
'whatsappReminders',
|
'whatsappReminders',
|
||||||
|
'fixedSlots',
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -58,7 +60,11 @@ export function PlanSelectStep({ form, plans }: PlanSelectStepProps) {
|
|||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
<h3 className="text-xl font-bold">{plan.name}</h3>
|
<h3 className="text-xl font-bold">{plan.name}</h3>
|
||||||
<div className="mt-1">
|
<div className="mt-1">
|
||||||
<span className="text-3xl font-bold">${plan.price}</span>
|
<span className="text-3xl font-bold">
|
||||||
|
{plan.currency === 'ARS'
|
||||||
|
? `AR$${plan.price.toLocaleString('es-AR')}`
|
||||||
|
: `$${plan.price.toFixed(2)}`}
|
||||||
|
</span>
|
||||||
<span className="text-sm text-muted-foreground">/mes</span>
|
<span className="text-sm text-muted-foreground">/mes</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -50,7 +50,14 @@ export function ReviewStep({ data, selectedPlan }: ReviewStepProps) {
|
|||||||
<Section title="Plan seleccionado">
|
<Section title="Plan seleccionado">
|
||||||
{selectedPlan ? (
|
{selectedPlan ? (
|
||||||
<>
|
<>
|
||||||
<Row label="Plan" value={`${selectedPlan.name} - $${selectedPlan.price}/mes`} />
|
<Row
|
||||||
|
label="Plan"
|
||||||
|
value={`${selectedPlan.name} - ${
|
||||||
|
selectedPlan.currency === 'ARS'
|
||||||
|
? `AR$${selectedPlan.price.toLocaleString('es-AR')}`
|
||||||
|
: `$${selectedPlan.price.toFixed(2)}`
|
||||||
|
}/mes`}
|
||||||
|
/>
|
||||||
<div className="mt-2 space-y-1">
|
<div className="mt-2 space-y-1">
|
||||||
{[
|
{[
|
||||||
['publicBookingPage', 'Página de booking pública'],
|
['publicBookingPage', 'Página de booking pública'],
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ type ShareWhatsappButtonProps = {
|
|||||||
confirmation: PublicBookingConfirmation;
|
confirmation: PublicBookingConfirmation;
|
||||||
};
|
};
|
||||||
|
|
||||||
function formatDateLabel(isoDate: string) {
|
function formatDateLabel(isoDate: string | null | undefined) {
|
||||||
|
if (!isoDate) return '';
|
||||||
const [year, month, day] = isoDate.split('-').map(Number);
|
const [year, month, day] = isoDate.split('-').map(Number);
|
||||||
const date = new Date(year, (month ?? 1) - 1, day ?? 1);
|
const date = new Date(year, (month ?? 1) - 1, day ?? 1);
|
||||||
|
|
||||||
@@ -18,16 +19,35 @@ function formatDateLabel(isoDate: string) {
|
|||||||
}).format(date);
|
}).format(date);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatBookingPrice(price: number): string {
|
||||||
|
if (price === 0) {
|
||||||
|
return 'Sin cargo';
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Intl.NumberFormat('es-AR', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'ARS',
|
||||||
|
maximumFractionDigits: Number.isInteger(price) ? 0 : 2,
|
||||||
|
}).format(price);
|
||||||
|
}
|
||||||
|
|
||||||
function createWhatsappMessage(confirmation: PublicBookingConfirmation) {
|
function createWhatsappMessage(confirmation: PublicBookingConfirmation) {
|
||||||
const dateLabel = formatDateLabel(confirmation.date);
|
const dateLabel = formatDateLabel(confirmation.date);
|
||||||
return [
|
const lines = ['Ya reservamos cancha para jugar.', `Complejo: ${confirmation.complexName}`];
|
||||||
'Ya reservamos cancha para jugar.',
|
|
||||||
`Complejo: ${confirmation.complexName}`,
|
if (confirmation.complexAddress) {
|
||||||
|
lines.push(`Direccion: ${confirmation.complexAddress}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push(
|
||||||
`Cancha: ${confirmation.courtName} (${confirmation.sport.name})`,
|
`Cancha: ${confirmation.courtName} (${confirmation.sport.name})`,
|
||||||
`Fecha: ${dateLabel}`,
|
`Fecha: ${dateLabel}`,
|
||||||
`Horario: ${confirmation.startTime} - ${confirmation.endTime}`,
|
`Horario: ${confirmation.startTime} - ${confirmation.endTime}`,
|
||||||
`Codigo de reserva: ${confirmation.bookingCode}`,
|
`Precio: ${formatBookingPrice(confirmation.price)}`,
|
||||||
].join('\n');
|
`Codigo de reserva: ${confirmation.bookingCode}`
|
||||||
|
);
|
||||||
|
|
||||||
|
return lines.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ShareWhatsappButton({ confirmation }: ShareWhatsappButtonProps) {
|
export function ShareWhatsappButton({ confirmation }: ShareWhatsappButtonProps) {
|
||||||
@@ -36,17 +56,28 @@ export function ShareWhatsappButton({ confirmation }: ShareWhatsappButtonProps)
|
|||||||
)}`;
|
)}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button type="button" variant="outline" className="h-11 w-full text-sm sm:text-base" asChild>
|
<Button
|
||||||
<a href={whatsappUrl} target="_blank" rel="noopener noreferrer">
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
className="h-12 w-full overflow-hidden rounded-xl border-slate-200 bg-white px-0 text-sm font-semibold text-slate-900 hover:bg-slate-50 sm:px-4 sm:text-base"
|
||||||
|
asChild
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href={whatsappUrl}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
aria-label="Compartir por WhatsApp"
|
||||||
|
title="Compartir por WhatsApp"
|
||||||
|
>
|
||||||
<svg
|
<svg
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
className="size-4"
|
className="size-5"
|
||||||
style={{ color: `#${siWhatsapp.hex}` }}
|
style={{ color: `#${siWhatsapp.hex}` }}
|
||||||
>
|
>
|
||||||
<path d={siWhatsapp.path} fill="currentColor" />
|
<path d={siWhatsapp.path} fill="currentColor" />
|
||||||
</svg>
|
</svg>
|
||||||
Compartir por WhatsApp
|
<span className="hidden sm:inline">Compartir por WhatsApp</span>
|
||||||
</a>
|
</a>
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,30 @@
|
|||||||
|
import PlayzerLogo from '@/assets/playzer-logo-transparent.svg';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { ApiClientError, apiClient } from '@/lib/api-client';
|
import { Input } from '@/components/ui/input';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import {
|
||||||
|
ResponsiveDialog,
|
||||||
|
ResponsiveDialogClose,
|
||||||
|
ResponsiveDialogContent,
|
||||||
|
ResponsiveDialogDescription,
|
||||||
|
ResponsiveDialogFooter,
|
||||||
|
ResponsiveDialogHeader,
|
||||||
|
ResponsiveDialogTitle,
|
||||||
|
} from '@/components/ui/responsive-dialog';
|
||||||
|
import { apiClient } from '@/lib/api-client';
|
||||||
|
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||||
import { useNavigate } from '@tanstack/react-router';
|
import { useNavigate } from '@tanstack/react-router';
|
||||||
|
import {
|
||||||
|
AlertCircle,
|
||||||
|
ArrowRight,
|
||||||
|
CalendarDays,
|
||||||
|
CheckCircle2,
|
||||||
|
Clock3,
|
||||||
|
LoaderCircle,
|
||||||
|
Receipt,
|
||||||
|
Trophy,
|
||||||
|
XCircle,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { useState } from 'react';
|
||||||
import { ShareWhatsappButton } from './components/share-whatsapp-button';
|
import { ShareWhatsappButton } from './components/share-whatsapp-button';
|
||||||
|
|
||||||
type PublicBookingConfirmationPageProps = {
|
type PublicBookingConfirmationPageProps = {
|
||||||
@@ -10,23 +33,34 @@ type PublicBookingConfirmationPageProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function extractMessage(error: unknown, fallback: string) {
|
function extractMessage(error: unknown, fallback: string) {
|
||||||
if (error instanceof ApiClientError) {
|
const msg = (error as { message?: string } | undefined)?.message;
|
||||||
return error.message || fallback;
|
return msg || fallback;
|
||||||
}
|
|
||||||
|
|
||||||
return fallback;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatDateLabel(isoDate: string) {
|
function formatDateLabel(isoDate: string | undefined | null) {
|
||||||
|
if (!isoDate) return '';
|
||||||
const [year, month, day] = isoDate.split('-').map(Number);
|
const [year, month, day] = isoDate.split('-').map(Number);
|
||||||
const date = new Date(year, (month ?? 1) - 1, day ?? 1);
|
const date = new Date(year, (month ?? 1) - 1, day ?? 1);
|
||||||
|
|
||||||
return new Intl.DateTimeFormat('es-AR', {
|
const label = new Intl.DateTimeFormat('es-AR', {
|
||||||
weekday: 'long',
|
weekday: 'long',
|
||||||
day: '2-digit',
|
day: '2-digit',
|
||||||
month: 'long',
|
month: 'long',
|
||||||
year: 'numeric',
|
|
||||||
}).format(date);
|
}).format(date);
|
||||||
|
|
||||||
|
return label.charAt(0).toUpperCase() + label.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatBookingPrice(price: number): string {
|
||||||
|
if (price === 0) {
|
||||||
|
return 'Sin cargo';
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Intl.NumberFormat('es-AR', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'ARS',
|
||||||
|
maximumFractionDigits: Number.isInteger(price) ? 0 : 2,
|
||||||
|
}).format(price);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PublicBookingConfirmationPage({
|
export function PublicBookingConfirmationPage({
|
||||||
@@ -34,82 +68,230 @@ export function PublicBookingConfirmationPage({
|
|||||||
bookingCode,
|
bookingCode,
|
||||||
}: PublicBookingConfirmationPageProps) {
|
}: PublicBookingConfirmationPageProps) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const [cancelDialogOpen, setCancelDialogOpen] = useState(false);
|
||||||
|
const [customerPhone, setCustomerPhone] = useState('');
|
||||||
|
const [cancelError, setCancelError] = useState<string | null>(null);
|
||||||
|
const [cancelledBooking, setCancelledBooking] = useState<{
|
||||||
|
date: string;
|
||||||
|
startTime: string;
|
||||||
|
endTime: string;
|
||||||
|
courtName: string;
|
||||||
|
sport: { name: string };
|
||||||
|
bookingCode: string;
|
||||||
|
price: number;
|
||||||
|
complexName: string;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
const confirmationQuery = useQuery({
|
const confirmationQuery = useQuery({
|
||||||
queryKey: ['public-booking-confirmation', complexSlug, bookingCode],
|
queryKey: ['public-booking-confirmation', complexSlug, bookingCode],
|
||||||
queryFn: () => apiClient.publicBookings.getConfirmation(complexSlug, bookingCode),
|
queryFn: () => apiClient.publicBookings.getConfirmation(complexSlug, bookingCode),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const cancelMutation = useMutation({
|
||||||
|
mutationFn: (phone: string) =>
|
||||||
|
apiClient.publicBookings.cancelPublic(complexSlug, {
|
||||||
|
bookingCode,
|
||||||
|
customerPhone: phone,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleCancelClick = () => {
|
||||||
|
cancelMutation.reset();
|
||||||
|
setCustomerPhone('');
|
||||||
|
setCancelError(null);
|
||||||
|
setCancelDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirmCancel = async () => {
|
||||||
|
setCancelError(null);
|
||||||
|
try {
|
||||||
|
const data = await cancelMutation.mutateAsync(customerPhone);
|
||||||
|
setCancelledBooking(data);
|
||||||
|
setCancelDialogOpen(false);
|
||||||
|
setCustomerPhone('');
|
||||||
|
} catch (error: unknown) {
|
||||||
|
console.error('[Cancel]', error);
|
||||||
|
setCancelError(
|
||||||
|
(error as { message?: string } | undefined)?.message ?? 'No pudimos cancelar la reserva.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isCancelled = cancelledBooking !== null;
|
||||||
|
const displayData = cancelledBooking ?? confirmationQuery.data;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="min-h-screen bg-linear-to-b from-background via-background to-primary/5">
|
<main className="min-h-screen bg-[#edf7f4] text-[#111827]">
|
||||||
<div className="mx-auto flex min-h-screen w-full max-w-3xl items-center px-3 py-8 sm:px-6">
|
<div className="mx-auto flex min-h-screen w-full max-w-3xl items-center px-4 py-8 sm:px-6">
|
||||||
<section className="w-full rounded-3xl border bg-card p-5 shadow-lg sm:p-8">
|
<section className="w-full rounded-[28px] border border-emerald-950/10 bg-white p-5 shadow-[0_24px_70px_rgba(15,23,42,0.12)] sm:p-8">
|
||||||
{confirmationQuery.isLoading && (
|
{confirmationQuery.isLoading && !cancelledBooking && (
|
||||||
<p className="text-sm text-muted-foreground">Cargando confirmacion...</p>
|
<div className="flex items-center gap-3 text-sm text-slate-500">
|
||||||
|
<LoaderCircle className="size-5 animate-spin text-emerald-600" />
|
||||||
|
Cargando confirmación...
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{confirmationQuery.isError && (
|
{confirmationQuery.isError && !cancelledBooking && (
|
||||||
<p className="text-sm text-destructive">
|
<div className="flex gap-3 rounded-2xl border border-red-200 bg-red-50 p-4 text-sm text-red-700">
|
||||||
{extractMessage(
|
<AlertCircle className="mt-0.5 size-5 shrink-0" />
|
||||||
confirmationQuery.error,
|
<p>
|
||||||
'No pudimos cargar la confirmacion de la reserva.'
|
{extractMessage(
|
||||||
)}
|
confirmationQuery.error,
|
||||||
</p>
|
'No pudimos cargar la confirmación de la reserva.'
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{confirmationQuery.data && (
|
{displayData && (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div>
|
<div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
||||||
<p className="text-xs font-medium tracking-[0.2em] text-muted-foreground uppercase">
|
<div>
|
||||||
Reserva confirmada
|
{isCancelled ? (
|
||||||
</p>
|
<div className="inline-flex items-center gap-2 rounded-full bg-red-50 px-3 py-1 text-xs font-semibold tracking-[0.14em] text-red-600 uppercase">
|
||||||
<h1 className="mt-2 text-2xl font-semibold sm:text-3xl">
|
<XCircle className="size-4" />
|
||||||
{confirmationQuery.data.complexName}
|
Reserva cancelada
|
||||||
</h1>
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="inline-flex items-center gap-2 rounded-full bg-emerald-50 px-3 py-1 text-xs font-semibold tracking-[0.14em] text-emerald-700 uppercase">
|
||||||
|
<CheckCircle2 className="size-4" />
|
||||||
|
Reserva confirmada
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<h1 className="mt-3 text-2xl font-bold tracking-tight sm:text-3xl">
|
||||||
|
{displayData.complexName}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<img src={PlayzerLogo} alt="Playzer" className="h-9 w-auto sm:h-10" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="rounded-2xl border border-primary/30 bg-primary/5 p-4 text-center sm:p-5">
|
<div className="rounded-[24px] border border-emerald-500/30 bg-emerald-50/80 p-5 sm:p-6">
|
||||||
<p className="text-xs text-muted-foreground">Codigo de reserva</p>
|
<div className="flex flex-col gap-5 sm:flex-row sm:items-end sm:justify-between">
|
||||||
<p className="mt-1 text-3xl font-bold tracking-[0.25em] text-primary sm:text-4xl">
|
<div>
|
||||||
{confirmationQuery.data.bookingCode}
|
<p className="flex items-center gap-2 text-sm font-semibold text-emerald-700">
|
||||||
</p>
|
<CalendarDays className="size-4" />
|
||||||
|
Tu turno
|
||||||
|
</p>
|
||||||
|
<p className="mt-3 text-2xl font-black leading-tight tracking-tight text-slate-950 sm:text-4xl">
|
||||||
|
{formatDateLabel(displayData.date)}
|
||||||
|
</p>
|
||||||
|
<p className="mt-2 flex items-center gap-2 text-3xl font-black leading-none text-emerald-700 sm:text-5xl">
|
||||||
|
<Clock3 className="size-7 sm:size-9" />
|
||||||
|
{displayData.startTime} - {displayData.endTime}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-2xl border border-emerald-600/20 bg-white/80 px-4 py-3 sm:min-w-40 sm:text-right">
|
||||||
|
<p className="text-xs font-medium text-slate-500">Precio del turno</p>
|
||||||
|
<p className="mt-1 text-2xl font-bold text-slate-950">
|
||||||
|
{formatBookingPrice(displayData.price)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="rounded-2xl border bg-background p-4 sm:p-5">
|
<div className="overflow-hidden rounded-[22px] border border-slate-200 bg-slate-50/70">
|
||||||
<p className="text-sm text-muted-foreground">Fecha</p>
|
<div className="grid gap-px bg-slate-200 sm:grid-cols-2">
|
||||||
<p className="mt-1 text-base font-medium capitalize sm:text-lg">
|
<div className="bg-white p-4">
|
||||||
{formatDateLabel(confirmationQuery.data.date)}
|
<p className="flex items-center gap-2 text-sm font-medium text-slate-500">
|
||||||
</p>
|
<Trophy className="size-4 text-emerald-600" />
|
||||||
|
Cancha
|
||||||
<p className="mt-4 text-sm text-muted-foreground">Horario</p>
|
</p>
|
||||||
<p className="mt-1 text-base font-medium sm:text-lg">
|
<p className="mt-2 text-base font-semibold text-slate-950">
|
||||||
{confirmationQuery.data.startTime} - {confirmationQuery.data.endTime}
|
{displayData.courtName} · {displayData.sport.name}
|
||||||
</p>
|
</p>
|
||||||
|
</div>
|
||||||
<p className="mt-4 text-sm text-muted-foreground">Cancha</p>
|
<div className="bg-white p-4">
|
||||||
<p className="mt-1 text-base font-medium sm:text-lg">
|
<p className="flex items-center gap-2 text-sm font-medium text-slate-500">
|
||||||
{confirmationQuery.data.courtName} · {confirmationQuery.data.sport.name}
|
<Receipt className="size-4 text-emerald-600" />
|
||||||
</p>
|
Código de reserva
|
||||||
|
</p>
|
||||||
|
<p className="mt-2 font-mono text-lg font-bold tracking-[0.18em] text-slate-950">
|
||||||
|
{displayData.bookingCode}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-3 sm:grid-cols-2">
|
{isCancelled ? (
|
||||||
<ShareWhatsappButton confirmation={confirmationQuery.data} />
|
<div className="rounded-2xl bg-red-50 p-4 text-center text-sm text-red-700">
|
||||||
<Button
|
Esta reserva fue cancelada. Te enviamos un email con los detalles de la
|
||||||
type="button"
|
cancelación.
|
||||||
className="h-11 w-full text-sm sm:text-base"
|
</div>
|
||||||
onClick={() => {
|
) : (
|
||||||
void navigate({
|
<div className="grid grid-cols-[48px_minmax(0,1fr)_minmax(0,1fr)] gap-2 sm:grid-cols-3 sm:gap-3">
|
||||||
to: '/$complexSlug/booking',
|
<ShareWhatsappButton confirmation={confirmationQuery.data!} />
|
||||||
params: { complexSlug },
|
<Button
|
||||||
});
|
type="button"
|
||||||
}}
|
variant="outline"
|
||||||
>
|
className="h-12 w-full rounded-xl border-red-200 text-sm font-semibold text-red-600 hover:bg-red-50 hover:text-red-700 sm:text-base"
|
||||||
Hacer otra reserva
|
onClick={handleCancelClick}
|
||||||
</Button>
|
>
|
||||||
</div>
|
Cancelar
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
className="h-12 w-full rounded-xl bg-emerald-600 text-sm font-semibold text-white hover:bg-emerald-500 sm:text-base"
|
||||||
|
onClick={() => {
|
||||||
|
void navigate({
|
||||||
|
to: '/$complexSlug/booking',
|
||||||
|
params: { complexSlug },
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="hidden sm:inline">Hacer otra reserva</span>
|
||||||
|
<span className="sm:hidden">Otra</span>
|
||||||
|
<ArrowRight className="size-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ResponsiveDialog open={cancelDialogOpen} onOpenChange={setCancelDialogOpen}>
|
||||||
|
<ResponsiveDialogContent>
|
||||||
|
<ResponsiveDialogHeader>
|
||||||
|
<ResponsiveDialogTitle>Cancelar reserva</ResponsiveDialogTitle>
|
||||||
|
<ResponsiveDialogDescription>
|
||||||
|
Ingresá el número de teléfono que usaste al hacer la reserva para confirmar la
|
||||||
|
cancelación.
|
||||||
|
</ResponsiveDialogDescription>
|
||||||
|
</ResponsiveDialogHeader>
|
||||||
|
|
||||||
|
<div className="px-6 pb-2">
|
||||||
|
<Input
|
||||||
|
type="tel"
|
||||||
|
placeholder="Ej: 1134567890"
|
||||||
|
value={customerPhone}
|
||||||
|
onChange={(e) => {
|
||||||
|
setCustomerPhone(e.target.value);
|
||||||
|
setCancelError(null);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{cancelError && <p className="mt-2 text-sm text-red-600">{cancelError}</p>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ResponsiveDialogFooter>
|
||||||
|
<ResponsiveDialogClose asChild>
|
||||||
|
<Button variant="outline">Volver</Button>
|
||||||
|
</ResponsiveDialogClose>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
disabled={!customerPhone.trim() || cancelMutation.isPending}
|
||||||
|
onClick={handleConfirmCancel}
|
||||||
|
>
|
||||||
|
{cancelMutation.isPending ? (
|
||||||
|
<LoaderCircle className="size-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
'Sí, cancelar reserva'
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</ResponsiveDialogFooter>
|
||||||
|
</ResponsiveDialogContent>
|
||||||
|
</ResponsiveDialog>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,13 +2,6 @@ import PlayzerIcon from '@/assets/playzer-favicon-512-transparent.png';
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Field, FieldError, FieldLabel } from '@/components/ui/field';
|
import { Field, FieldError, FieldLabel } from '@/components/ui/field';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from '@/components/ui/select';
|
|
||||||
import {
|
import {
|
||||||
Stepper,
|
Stepper,
|
||||||
StepperIndicator,
|
StepperIndicator,
|
||||||
@@ -33,7 +26,6 @@ import {
|
|||||||
Check,
|
Check,
|
||||||
ChevronLeft,
|
ChevronLeft,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
Clock3,
|
|
||||||
Dumbbell,
|
Dumbbell,
|
||||||
Lock,
|
Lock,
|
||||||
MapPin,
|
MapPin,
|
||||||
@@ -41,12 +33,10 @@ import {
|
|||||||
Moon,
|
Moon,
|
||||||
Share2,
|
Share2,
|
||||||
ShieldCheck,
|
ShieldCheck,
|
||||||
Shirt,
|
|
||||||
Sparkles,
|
Sparkles,
|
||||||
Sun,
|
Sun,
|
||||||
Trophy,
|
Trophy,
|
||||||
Users,
|
Users,
|
||||||
Wrench,
|
|
||||||
Zap,
|
Zap,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
@@ -69,6 +59,7 @@ const bookingFormSchema = z.object({
|
|||||||
.trim()
|
.trim()
|
||||||
.min(6, 'Ingresá un teléfono válido.')
|
.min(6, 'Ingresá un teléfono válido.')
|
||||||
.max(30, 'El teléfono no puede superar los 30 caracteres.'),
|
.max(30, 'El teléfono no puede superar los 30 caracteres.'),
|
||||||
|
customerEmail: z.string().email('Ingresá un email válido.'),
|
||||||
});
|
});
|
||||||
|
|
||||||
type BookingFormValues = z.infer<typeof bookingFormSchema>;
|
type BookingFormValues = z.infer<typeof bookingFormSchema>;
|
||||||
@@ -84,6 +75,7 @@ type SelectedSlot = {
|
|||||||
sportName: string;
|
sportName: string;
|
||||||
startTime: string;
|
startTime: string;
|
||||||
endTime: string;
|
endTime: string;
|
||||||
|
price: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type BookingShellProps = {
|
type BookingShellProps = {
|
||||||
@@ -186,6 +178,18 @@ function extractMessage(error: unknown, fallback: string) {
|
|||||||
return fallback;
|
return fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatBookingPrice(price: number): string {
|
||||||
|
if (price === 0) {
|
||||||
|
return 'Sin cargo';
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Intl.NumberFormat('es-AR', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'ARS',
|
||||||
|
maximumFractionDigits: Number.isInteger(price) ? 0 : 2,
|
||||||
|
}).format(price);
|
||||||
|
}
|
||||||
|
|
||||||
function getStep(selectedSlot: SelectedSlot | null, isValid: boolean) {
|
function getStep(selectedSlot: SelectedSlot | null, isValid: boolean) {
|
||||||
if (!selectedSlot) return 1;
|
if (!selectedSlot) return 1;
|
||||||
if (!isValid) return 3;
|
if (!isValid) return 3;
|
||||||
@@ -369,7 +373,6 @@ function Legend() {
|
|||||||
<div className="flex flex-wrap gap-4 text-xs text-white/70">
|
<div className="flex flex-wrap gap-4 text-xs text-white/70">
|
||||||
<LegendDot color="bg-emerald-500" label="Libre" />
|
<LegendDot color="bg-emerald-500" label="Libre" />
|
||||||
<LegendDot color="bg-blue-500" label="Reservado" />
|
<LegendDot color="bg-blue-500" label="Reservado" />
|
||||||
<LegendDot color="bg-red-500" label="Mantenimiento" />
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -409,7 +412,7 @@ function PublicBookingPageChrome({
|
|||||||
if (mobile) {
|
if (mobile) {
|
||||||
return (
|
return (
|
||||||
<main className={`${themeClass} min-h-screen bg-[#050d14] text-white`}>
|
<main className={`${themeClass} min-h-screen bg-[#050d14] text-white`}>
|
||||||
<div className="public-booking-frame mx-auto min-h-screen max-w-[430px] bg-[radial-gradient(circle_at_50%_0%,rgba(17,185,129,0.16),transparent_34%),linear-gradient(180deg,#07131d_0%,#071018_47%,#050b11_100%)] px-4 pb-24 pt-7 shadow-2xl shadow-black">
|
<div className="public-booking-frame mx-auto min-h-screen max-w-[430px] bg-[radial-gradient(circle_at_50%_0%,rgba(17,185,129,0.16),transparent_34%),linear-gradient(180deg,#07131d_0%,#071018_47%,#050b11_100%)] px-4 pb-0 pt-7 shadow-2xl shadow-black">
|
||||||
<header className="flex items-center justify-between">
|
<header className="flex items-center justify-between">
|
||||||
<PlayzerBrand compact />
|
<PlayzerBrand compact />
|
||||||
<PublicBookingThemeToggle />
|
<PublicBookingThemeToggle />
|
||||||
@@ -436,8 +439,6 @@ function PublicBookingPageChrome({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{children}
|
{children}
|
||||||
|
|
||||||
<MobileBottomNav />
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
@@ -548,8 +549,7 @@ function PublicBookingDesktop(props: BookingShellProps) {
|
|||||||
const currentStep = getStep(selectedSlot, props.form.formState.isValid);
|
const currentStep = getStep(selectedSlot, props.form.formState.isValid);
|
||||||
const completedSteps = getCompletedSteps(selectedSlot, props.form.formState.isValid);
|
const completedSteps = getCompletedSteps(selectedSlot, props.form.formState.isValid);
|
||||||
const selectedDay = dayOptions.find((option) => option.value === selectedDate);
|
const selectedDay = dayOptions.find((option) => option.value === selectedDate);
|
||||||
const currentDayIndex = dayOptions.findIndex((option) => option.value === selectedDate);
|
const canGoBackButton = canGoBack;
|
||||||
const canGoBackButton = canGoBack || currentDayIndex > 0;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PublicBookingPageChrome
|
<PublicBookingPageChrome
|
||||||
@@ -638,48 +638,49 @@ function PublicBookingDesktop(props: BookingShellProps) {
|
|||||||
)}
|
)}
|
||||||
{isError && <StatusPanel>{errorMessage}</StatusPanel>}
|
{isError && <StatusPanel>{errorMessage}</StatusPanel>}
|
||||||
{isLoading && <StatusPanel>Buscando disponibilidad...</StatusPanel>}
|
{isLoading && <StatusPanel>Buscando disponibilidad...</StatusPanel>}
|
||||||
<Panel className="overflow-hidden">
|
{selectedCourt && (
|
||||||
<div className="flex items-center border-b border-white/10 px-6 py-5">
|
<>
|
||||||
<CourtHeader court={selectedCourt} complexName={props.complexName} />
|
<Panel className="overflow-hidden">
|
||||||
<div className="ml-auto flex items-center overflow-hidden rounded-md border border-white/10 bg-white/[0.035] text-sm">
|
<div className="flex items-center border-b border-white/10 px-6 py-5">
|
||||||
<button
|
<CourtHeader court={selectedCourt} complexName={props.complexName} />
|
||||||
type="button"
|
<div className="ml-auto flex items-center overflow-hidden rounded-md border border-white/10 bg-white/[0.035] text-sm">
|
||||||
className="flex size-11 items-center justify-center border-r border-white/10 transition hover:bg-white/[0.06]"
|
<button
|
||||||
onClick={props.onPreviousDay}
|
type="button"
|
||||||
disabled={!canGoBackButton}
|
className="flex size-11 items-center justify-center border-r border-white/10 transition hover:bg-white/[0.06]"
|
||||||
aria-label="Día anterior"
|
onClick={props.onPreviousDay}
|
||||||
>
|
disabled={!canGoBackButton}
|
||||||
<ChevronLeft className="size-5" />
|
aria-label="Día anterior"
|
||||||
</button>
|
>
|
||||||
<div className="flex h-11 min-w-[250px] items-center justify-center gap-2 px-4">
|
<ChevronLeft className="size-5" />
|
||||||
<CalendarDays className="size-4 text-white/76" />
|
</button>
|
||||||
<span>{selectedDay?.longLabel ?? selectedDate}</span>
|
<div className="flex h-11 min-w-[250px] items-center justify-center gap-2 px-4">
|
||||||
|
<CalendarDays className="size-4 text-white/76" />
|
||||||
|
<span>{selectedDay?.longLabel ?? selectedDate}</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="flex size-11 items-center justify-center border-l border-white/10 transition hover:bg-white/[0.06]"
|
||||||
|
onClick={props.onNextDay}
|
||||||
|
aria-label="Día siguiente"
|
||||||
|
>
|
||||||
|
<ChevronRight className="size-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="flex size-11 items-center justify-center border-l border-white/10 transition hover:bg-white/[0.06]"
|
|
||||||
onClick={props.onNextDay}
|
|
||||||
aria-label="Día siguiente"
|
|
||||||
>
|
|
||||||
<ChevronRight className="size-5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
<Legend />
|
<Legend />
|
||||||
<BookingTimeline
|
<BookingTimeline
|
||||||
court={selectedCourt}
|
court={selectedCourt}
|
||||||
selectedSlot={selectedSlot}
|
selectedSlot={selectedSlot}
|
||||||
onSelectSlot={onSelectSlot}
|
onSelectSlot={onSelectSlot}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|
||||||
<div className="grid grid-cols-[minmax(0,1fr)_minmax(340px,0.95fr)] gap-4">
|
<SelectedSlotCard {...props} />
|
||||||
<CourtDetails court={selectedCourt} />
|
</>
|
||||||
<SelectedSlotCard {...props} />
|
)}
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</PublicBookingPageChrome>
|
</PublicBookingPageChrome>
|
||||||
@@ -711,16 +712,19 @@ function PublicBookingMobile(props: BookingShellProps) {
|
|||||||
options={props.sports.map((sport) => ({ value: sport.id, label: sport.name }))}
|
options={props.sports.map((sport) => ({ value: sport.id, label: sport.name }))}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<MobileSelect
|
{props.courts.length > 0 ? (
|
||||||
label="Cancha"
|
<MobileCourtPicker
|
||||||
value={selectedCourt?.courtId ?? ''}
|
courts={props.courts}
|
||||||
placeholder="Seleccioná"
|
selectedCourtId={selectedCourt?.courtId}
|
||||||
onValueChange={props.onSelectCourt}
|
onSelectCourt={props.onSelectCourt}
|
||||||
options={props.courts.map((court) => ({
|
/>
|
||||||
value: court.courtId,
|
) : (
|
||||||
label: court.courtName,
|
!props.isLoading && (
|
||||||
}))}
|
<p className="rounded-md border border-white/10 bg-white/[0.035] p-3 text-sm text-white/62">
|
||||||
/>
|
No hay canchas disponibles para esta fecha.
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|
||||||
@@ -756,25 +760,23 @@ function PublicBookingMobile(props: BookingShellProps) {
|
|||||||
)}
|
)}
|
||||||
{props.isLoading && <StatusPanel>Buscando disponibilidad...</StatusPanel>}
|
{props.isLoading && <StatusPanel>Buscando disponibilidad...</StatusPanel>}
|
||||||
{props.isError && <StatusPanel>{props.errorMessage}</StatusPanel>}
|
{props.isError && <StatusPanel>{props.errorMessage}</StatusPanel>}
|
||||||
<Panel className="overflow-hidden">
|
{selectedCourt && (
|
||||||
<div className="p-3">
|
<Panel className="overflow-hidden">
|
||||||
<CourtHeader court={selectedCourt} compact />
|
<div className="p-3">
|
||||||
<div className="mt-3">
|
<CourtHeader court={selectedCourt} compact />
|
||||||
<Legend />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="border-t border-white/10 p-3">
|
||||||
<div className="border-t border-white/10 px-0 py-3">
|
<MobileAvailableSlots
|
||||||
<BookingTimeline
|
court={selectedCourt}
|
||||||
court={selectedCourt}
|
selectedSlot={props.selectedSlot}
|
||||||
selectedSlot={props.selectedSlot}
|
onSelectSlot={props.onSelectSlot}
|
||||||
onSelectSlot={props.onSelectSlot}
|
/>
|
||||||
compact
|
</div>
|
||||||
/>
|
<div className="px-3 pb-3">
|
||||||
</div>
|
<SelectedSlotCard {...props} compact />
|
||||||
<div className="px-3 pb-3">
|
</div>
|
||||||
<SelectedSlotCard {...props} compact />
|
</Panel>
|
||||||
</div>
|
)}
|
||||||
</Panel>
|
|
||||||
</div>
|
</div>
|
||||||
</PublicBookingPageChrome>
|
</PublicBookingPageChrome>
|
||||||
);
|
);
|
||||||
@@ -876,34 +878,119 @@ function MobileSportSelector({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function MobileSelect({
|
function MobileCourtPicker({
|
||||||
label,
|
courts,
|
||||||
value,
|
selectedCourtId,
|
||||||
placeholder,
|
onSelectCourt,
|
||||||
options,
|
|
||||||
onValueChange,
|
|
||||||
}: {
|
}: {
|
||||||
label: string;
|
courts: PublicAvailabilityCourt[];
|
||||||
value: string;
|
selectedCourtId?: string;
|
||||||
placeholder: string;
|
onSelectCourt: (courtId: string) => void;
|
||||||
options: { value: string; label: string }[];
|
|
||||||
onValueChange: (value: string) => void;
|
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-[88px_minmax(0,1fr)] items-center border-b border-white/10 bg-white/[0.025] px-3 py-2 last:border-b-0">
|
<div className="border-b border-white/10 bg-white/[0.025] px-3 py-3 last:border-b-0">
|
||||||
<span className="text-xs text-white/66">{label}</span>
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<Select value={value} onValueChange={onValueChange}>
|
{courts.map((court) => {
|
||||||
<SelectTrigger className="h-8 border-0 bg-transparent px-0 text-xs text-white shadow-none">
|
const isSelected = selectedCourtId === court.courtId;
|
||||||
<SelectValue placeholder={placeholder} />
|
|
||||||
</SelectTrigger>
|
return (
|
||||||
<SelectContent>
|
<button
|
||||||
{options.map((option) => (
|
key={court.courtId}
|
||||||
<SelectItem key={option.value} value={option.value}>
|
type="button"
|
||||||
{option.label}
|
onClick={() => onSelectCourt(court.courtId)}
|
||||||
</SelectItem>
|
aria-pressed={isSelected}
|
||||||
))}
|
className={`flex min-h-16 items-start gap-2 rounded-md border p-3 text-left transition ${
|
||||||
</SelectContent>
|
isSelected
|
||||||
</Select>
|
? 'border-emerald-400 bg-emerald-500/20 text-white shadow-[inset_0_0_18px_rgba(16,185,129,0.12)]'
|
||||||
|
: 'border-white/10 bg-white/[0.045] text-white/72 active:border-white/25 active:bg-white/[0.075]'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={`mt-0.5 flex size-5 shrink-0 items-center justify-center rounded-full border ${
|
||||||
|
isSelected
|
||||||
|
? 'border-emerald-300 bg-emerald-400 text-[#061019]'
|
||||||
|
: 'border-white/18 text-transparent'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Check className="size-3.5" />
|
||||||
|
</span>
|
||||||
|
<span className="min-w-0">
|
||||||
|
<span className="block truncate text-sm font-semibold">{court.courtName}</span>
|
||||||
|
<span className="mt-0.5 block truncate text-xs text-white/52">
|
||||||
|
{court.sport.name}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function MobileAvailableSlots({
|
||||||
|
court,
|
||||||
|
selectedSlot,
|
||||||
|
onSelectSlot,
|
||||||
|
}: {
|
||||||
|
court: PublicAvailabilityCourt;
|
||||||
|
selectedSlot: SelectedSlot | null;
|
||||||
|
onSelectSlot: (slot: SelectedSlot) => void;
|
||||||
|
}) {
|
||||||
|
const slots = court.availableSlots;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="flex items-end justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-base font-semibold">Horarios disponibles</h3>
|
||||||
|
<p className="mt-0.5 text-xs text-white/58">
|
||||||
|
Turnos de {court.slotDurationMinutes} minutos
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<span className="shrink-0 text-xs font-medium text-emerald-300/90">
|
||||||
|
{slots.length} {slots.length === 1 ? 'turno' : 'turnos'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{slots.length > 0 ? (
|
||||||
|
<div className="mt-3 grid grid-cols-3 gap-2">
|
||||||
|
{slots.map((slot) => {
|
||||||
|
const selected = isSlotSelected(slot, court.courtId, selectedSlot);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={`${court.courtId}-${slot.startTime}`}
|
||||||
|
type="button"
|
||||||
|
onClick={() =>
|
||||||
|
onSelectSlot({
|
||||||
|
courtId: court.courtId,
|
||||||
|
courtName: court.courtName,
|
||||||
|
sportId: court.sport.id,
|
||||||
|
sportName: court.sport.name,
|
||||||
|
startTime: slot.startTime,
|
||||||
|
endTime: slot.endTime,
|
||||||
|
price: slot.price,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
aria-pressed={selected}
|
||||||
|
className={`flex h-12 items-center justify-center gap-1.5 rounded-md border text-sm font-bold transition ${
|
||||||
|
selected
|
||||||
|
? 'border-emerald-300 bg-emerald-400 text-[#061019] shadow-lg shadow-emerald-500/20'
|
||||||
|
: 'border-emerald-500/45 bg-emerald-500/12 text-white active:bg-emerald-500/24'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{selected && <Check className="size-4" />}
|
||||||
|
{slot.startTime}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="mt-3 rounded-md border border-white/10 bg-white/[0.035] p-3 text-sm text-white/62">
|
||||||
|
No hay horarios disponibles para esta cancha.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1011,6 +1098,7 @@ function BookingTimeline({
|
|||||||
sportName: court.sport.name,
|
sportName: court.sport.name,
|
||||||
startTime: slot.startTime,
|
startTime: slot.startTime,
|
||||||
endTime: slot.endTime,
|
endTime: slot.endTime,
|
||||||
|
price: slot.price,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
className={`absolute top-7 flex h-12 min-w-[50px] items-center justify-center rounded border text-xs transition ${
|
className={`absolute top-7 flex h-12 min-w-[50px] items-center justify-center rounded border text-xs transition ${
|
||||||
@@ -1036,19 +1124,13 @@ function BookingTimeline({
|
|||||||
|
|
||||||
const left = ((end - range.start) / total) * 100;
|
const left = ((end - range.start) / total) * 100;
|
||||||
const width = ((Math.min(nextStart, range.end) - end) / total) * 100;
|
const width = ((Math.min(nextStart, range.end) - end) / total) * 100;
|
||||||
const maintenance = index % 2 === 1;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={`${court?.courtId}-blocked-${slot.startTime}`}
|
key={`${court?.courtId}-blocked-${slot.startTime}`}
|
||||||
className={`absolute top-7 flex h-12 min-w-[44px] items-center justify-center rounded border ${
|
className="absolute top-7 flex h-12 min-w-[44px] items-center justify-center rounded border border-blue-500/70 bg-blue-500/24 text-blue-100"
|
||||||
maintenance
|
|
||||||
? 'border-red-500/80 bg-red-500/28 text-red-100'
|
|
||||||
: 'border-blue-500/70 bg-blue-500/24 text-blue-100'
|
|
||||||
}`}
|
|
||||||
style={{ left: `${left}%`, width: `${Math.max(width - 0.8, 4.5)}%` }}
|
style={{ left: `${left}%`, width: `${Math.max(width - 0.8, 4.5)}%` }}
|
||||||
>
|
>
|
||||||
{maintenance ? <Wrench className="size-4" /> : <Lock className="size-4" />}
|
<Lock className="size-4" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -1071,41 +1153,6 @@ function BookingTimeline({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CourtDetails({ court }: { court?: PublicAvailabilityCourt }) {
|
|
||||||
return (
|
|
||||||
<Panel className="p-6">
|
|
||||||
<div className="flex items-center gap-6">
|
|
||||||
<div className="hidden size-24 items-center justify-center rounded border border-white/16 text-white/45 md:flex">
|
|
||||||
<MapPin className="size-14" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 className="text-lg font-semibold">
|
|
||||||
{court?.courtName ? `${court.courtName} de ${court.sport.name}` : 'Cancha'}
|
|
||||||
</h3>
|
|
||||||
<div className="mt-4 space-y-2 text-sm text-white/62">
|
|
||||||
<p className="flex items-center gap-2">
|
|
||||||
<Users className="size-4 text-emerald-400" />
|
|
||||||
Superficie: Césped sintético
|
|
||||||
</p>
|
|
||||||
<p className="flex items-center gap-2">
|
|
||||||
<Dumbbell className="size-4 text-emerald-400" />
|
|
||||||
Turnos de {court?.slotDurationMinutes ?? 60} minutos
|
|
||||||
</p>
|
|
||||||
<p className="flex items-center gap-2">
|
|
||||||
<Sparkles className="size-4 text-emerald-400" />
|
|
||||||
Iluminación LED
|
|
||||||
</p>
|
|
||||||
<p className="flex items-center gap-2">
|
|
||||||
<Shirt className="size-4 text-emerald-400" />
|
|
||||||
Vestuarios disponibles
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Panel>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function SelectedSlotCard(props: BookingShellProps & { compact?: boolean }) {
|
function SelectedSlotCard(props: BookingShellProps & { compact?: boolean }) {
|
||||||
const {
|
const {
|
||||||
selectedSlot,
|
selectedSlot,
|
||||||
@@ -1127,9 +1174,11 @@ function SelectedSlotCard(props: BookingShellProps & { compact?: boolean }) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!selectedSlot) return;
|
if (!selectedSlot) return;
|
||||||
|
|
||||||
window.requestAnimationFrame(() => {
|
const input = customerNameInputRef.current;
|
||||||
customerNameInputRef.current?.focus();
|
if (!input) return;
|
||||||
});
|
|
||||||
|
input.focus();
|
||||||
|
input.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
}, [selectedSlot]);
|
}, [selectedSlot]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -1155,6 +1204,13 @@ function SelectedSlotCard(props: BookingShellProps & { compact?: boolean }) {
|
|||||||
className={compact ? 'mt-4 space-y-3' : 'mt-5 grid gap-3'}
|
className={compact ? 'mt-4 space-y-3' : 'mt-5 grid gap-3'}
|
||||||
onSubmit={handleSubmit(onSubmit)}
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
>
|
>
|
||||||
|
<div className="flex items-center justify-between gap-3 rounded-md border border-emerald-400/20 bg-emerald-400/[0.06] px-3 py-2.5">
|
||||||
|
<span className="text-sm text-white/72">Precio del turno</span>
|
||||||
|
<span className="shrink-0 text-base font-bold text-white">
|
||||||
|
{formatBookingPrice(selectedSlot.price)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className={compact ? 'space-y-3' : 'grid grid-cols-2 gap-3'}>
|
<div className={compact ? 'space-y-3' : 'grid grid-cols-2 gap-3'}>
|
||||||
<Field data-invalid={Boolean(errors.customerName)}>
|
<Field data-invalid={Boolean(errors.customerName)}>
|
||||||
<FieldLabel htmlFor="customerName" className="text-white/76">
|
<FieldLabel htmlFor="customerName" className="text-white/76">
|
||||||
@@ -1190,6 +1246,24 @@ function SelectedSlotCard(props: BookingShellProps & { compact?: boolean }) {
|
|||||||
</Field>
|
</Field>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Field data-invalid={Boolean(errors.customerEmail)}>
|
||||||
|
<FieldLabel htmlFor="customerEmail" className="text-white/76">
|
||||||
|
Email
|
||||||
|
</FieldLabel>
|
||||||
|
<Input
|
||||||
|
id="customerEmail"
|
||||||
|
type="email"
|
||||||
|
placeholder="Ej: juan@ejemplo.com"
|
||||||
|
aria-invalid={Boolean(errors.customerEmail)}
|
||||||
|
className="border-white/10 bg-white/[0.045] text-white placeholder:text-white/32"
|
||||||
|
{...register('customerEmail')}
|
||||||
|
/>
|
||||||
|
<p className="mt-1 text-xs text-white/40">
|
||||||
|
Te enviaremos la confirmación de la reserva por email.
|
||||||
|
</p>
|
||||||
|
<FieldError errors={[errors.customerEmail]} />
|
||||||
|
</Field>
|
||||||
|
|
||||||
{createError && <p className="text-sm text-red-300">{createError}</p>}
|
{createError && <p className="text-sm text-red-300">{createError}</p>}
|
||||||
{navigationError && <p className="text-sm text-red-300">{navigationError}</p>}
|
{navigationError && <p className="text-sm text-red-300">{navigationError}</p>}
|
||||||
|
|
||||||
@@ -1207,40 +1281,6 @@ function SelectedSlotCard(props: BookingShellProps & { compact?: boolean }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function MobileBottomNav() {
|
|
||||||
const items = [
|
|
||||||
{ label: 'Reservar', icon: CalendarDays, active: true },
|
|
||||||
{ label: 'Canchas', icon: Building2 },
|
|
||||||
{ label: 'Cómo funciona', icon: Clock3 },
|
|
||||||
{ label: 'Contacto', icon: Users },
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<nav className="fixed inset-x-0 bottom-0 z-40 mx-auto max-w-[430px] border-t border-emerald-500/30 bg-[#061019]/94 px-4 py-3 backdrop-blur">
|
|
||||||
<div className="grid grid-cols-4 gap-1">
|
|
||||||
{items.map((item) => {
|
|
||||||
const Icon = item.icon;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<a
|
|
||||||
key={item.label}
|
|
||||||
href={
|
|
||||||
item.label === 'Reservar' ? '#' : `#${item.label.toLowerCase().replace(' ', '-')}`
|
|
||||||
}
|
|
||||||
className={`flex flex-col items-center gap-1 text-[11px] ${
|
|
||||||
item.active ? 'text-emerald-400' : 'text-white/58'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<Icon className="size-5" />
|
|
||||||
{item.label}
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function PublicBookingPage({ complexSlug }: PublicBookingPageProps) {
|
export function PublicBookingPage({ complexSlug }: PublicBookingPageProps) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
@@ -1305,6 +1345,7 @@ export function PublicBookingPage({ complexSlug }: PublicBookingPageProps) {
|
|||||||
defaultValues: {
|
defaultValues: {
|
||||||
customerName: '',
|
customerName: '',
|
||||||
customerPhone: '',
|
customerPhone: '',
|
||||||
|
customerEmail: '',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1321,6 +1362,7 @@ export function PublicBookingPage({ complexSlug }: PublicBookingPageProps) {
|
|||||||
startTime: selectedSlot.startTime,
|
startTime: selectedSlot.startTime,
|
||||||
customerName: payload.customerName,
|
customerName: payload.customerName,
|
||||||
customerPhone: payload.customerPhone,
|
customerPhone: payload.customerPhone,
|
||||||
|
customerEmail: payload.customerEmail,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -1462,7 +1504,8 @@ export function PublicBookingPage({ complexSlug }: PublicBookingPageProps) {
|
|||||||
visibleCourts.length,
|
visibleCourts.length,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const canGoBack = windowStartOffset > 0;
|
const currentDayIndex = dayOptions.findIndex((option) => option.value === selectedDate);
|
||||||
|
const canGoBack = windowStartOffset > 0 || currentDayIndex > 0;
|
||||||
|
|
||||||
const selectDate = (date: string) => {
|
const selectDate = (date: string) => {
|
||||||
setSelectedDate(date);
|
setSelectedDate(date);
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user