From 981783babb07be33b7a20b034c28b7a21569f81d Mon Sep 17 00:00:00 2001 From: Ivan Date: Sun, 1 Feb 2026 06:17:02 +0000 Subject: [PATCH] feat(db): add Prisma schema with all models Configure Prisma ORM with PostgreSQL database schema including: - Organization, Site, Court models for multi-tenancy - User with role-based access and Client for customers - Booking and Payment models for reservations - MembershipPlan and Membership for subscriptions - Product, Sale, SaleItem, CashRegister for POS - Tournament, TournamentInscription, Match for competitions - All necessary enums, relations, indexes, and cascading deletes - Prisma client singleton for Next.js Co-Authored-By: Claude Opus 4.5 --- apps/web/.env.example | 4 + apps/web/lib/db.ts | 18 ++ apps/web/package.json | 7 +- apps/web/prisma/schema.prisma | 546 ++++++++++++++++++++++++++++++++++ 4 files changed, 574 insertions(+), 1 deletion(-) create mode 100644 apps/web/.env.example create mode 100644 apps/web/lib/db.ts create mode 100644 apps/web/prisma/schema.prisma diff --git a/apps/web/.env.example b/apps/web/.env.example new file mode 100644 index 0000000..eb2c8ef --- /dev/null +++ b/apps/web/.env.example @@ -0,0 +1,4 @@ +DATABASE_URL="postgresql://user:password@localhost:5432/padel_pro?schema=public" +NEXTAUTH_SECRET="your-secret-key-here" +NEXTAUTH_URL="http://localhost:3000" +NEXT_PUBLIC_APP_URL="http://localhost:3000" diff --git a/apps/web/lib/db.ts b/apps/web/lib/db.ts new file mode 100644 index 0000000..6b75508 --- /dev/null +++ b/apps/web/lib/db.ts @@ -0,0 +1,18 @@ +import { PrismaClient } from '@prisma/client'; + +declare global { + // eslint-disable-next-line no-var + var prisma: PrismaClient | undefined; +} + +export const db = + globalThis.prisma || + new PrismaClient({ + log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'], + }); + +if (process.env.NODE_ENV !== 'production') { + globalThis.prisma = db; +} + +export default db; diff --git a/apps/web/package.json b/apps/web/package.json index 36e2f32..ba70029 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -7,9 +7,13 @@ "build": "next build", "start": "next start", "lint": "next lint", - "type-check": "tsc --noEmit" + "type-check": "tsc --noEmit", + "db:generate": "prisma generate", + "db:push": "prisma db push", + "db:studio": "prisma studio" }, "dependencies": { + "@prisma/client": "^5.10.0", "next": "14.2.0", "react": "^18.2.0", "react-dom": "^18.2.0" @@ -20,6 +24,7 @@ "@types/react-dom": "^18.2.0", "autoprefixer": "^10.4.17", "postcss": "^8.4.35", + "prisma": "^5.10.0", "tailwindcss": "^3.4.1", "typescript": "^5.3.3" } diff --git a/apps/web/prisma/schema.prisma b/apps/web/prisma/schema.prisma new file mode 100644 index 0000000..5602e0b --- /dev/null +++ b/apps/web/prisma/schema.prisma @@ -0,0 +1,546 @@ +// This is your Prisma schema file, +// learn more about it in the docs: https://pris.ly/d/prisma-schema + +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +// ============================================================================= +// ENUMS +// ============================================================================= + +enum UserRole { + SUPER_ADMIN + ORG_ADMIN + SITE_ADMIN + RECEPTIONIST + TRAINER +} + +enum CourtType { + INDOOR + OUTDOOR + COVERED +} + +enum CourtStatus { + AVAILABLE + MAINTENANCE + CLOSED +} + +enum BookingStatus { + PENDING + CONFIRMED + CANCELLED + COMPLETED + NO_SHOW +} + +enum PaymentType { + CASH + CARD + TRANSFER + MEMBERSHIP + FREE +} + +enum MembershipStatus { + ACTIVE + EXPIRED + CANCELLED + SUSPENDED +} + +enum TournamentType { + AMERICANO + MEXICANO + BRACKET + ROUND_ROBIN + LEAGUE +} + +enum TournamentStatus { + DRAFT + REGISTRATION_OPEN + REGISTRATION_CLOSED + IN_PROGRESS + COMPLETED + CANCELLED +} + +enum MatchStatus { + SCHEDULED + IN_PROGRESS + COMPLETED + CANCELLED + WALKOVER +} + +// ============================================================================= +// ORGANIZATION & SITES +// ============================================================================= + +model Organization { + id String @id @default(cuid()) + name String + slug String @unique + logo String? + settings Json @default("{}") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + sites Site[] + users User[] + clients Client[] + memberships MembershipPlan[] + products Product[] + categories ProductCategory[] + tournaments Tournament[] + + @@index([slug]) +} + +model Site { + id String @id @default(cuid()) + organizationId String + name String + slug String + address String? + phone String? + email String? + timezone String @default("Europe/Madrid") + openTime String @default("08:00") + closeTime String @default("22:00") + settings Json @default("{}") + isActive Boolean @default(true) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) + courts Court[] + users User[] + bookings Booking[] + cashRegisters CashRegister[] + tournaments Tournament[] + + @@unique([organizationId, slug]) + @@index([organizationId]) + @@index([slug]) +} + +model Court { + id String @id @default(cuid()) + siteId String + name String + type CourtType @default(INDOOR) + status CourtStatus @default(AVAILABLE) + pricePerHour Decimal @db.Decimal(10, 2) + description String? + features String[] @default([]) + displayOrder Int @default(0) + isActive Boolean @default(true) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + site Site @relation(fields: [siteId], references: [id], onDelete: Cascade) + bookings Booking[] + matches Match[] + + @@index([siteId]) + @@index([status]) +} + +// ============================================================================= +// USERS & CLIENTS +// ============================================================================= + +model User { + id String @id @default(cuid()) + organizationId String + email String + password String + firstName String + lastName String + role UserRole @default(RECEPTIONIST) + phone String? + avatar String? + siteIds String[] @default([]) + isActive Boolean @default(true) + lastLogin DateTime? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) + sites Site[] + bookingsCreated Booking[] @relation("BookingCreatedBy") + salesCreated Sale[] @relation("SaleCreatedBy") + cashRegisters CashRegister[] + + @@unique([organizationId, email]) + @@index([organizationId]) + @@index([email]) + @@index([role]) +} + +model Client { + id String @id @default(cuid()) + organizationId String + email String? + phone String? + firstName String + lastName String + dni String? + dateOfBirth DateTime? + address String? + notes String? + avatar String? + level String? + preferredHand String? + emergencyContact String? + emergencyPhone String? + tags String[] @default([]) + isActive Boolean @default(true) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) + memberships Membership[] + bookings Booking[] + payments Payment[] + sales Sale[] + inscriptions TournamentInscription[] + + @@unique([organizationId, email]) + @@unique([organizationId, dni]) + @@index([organizationId]) + @@index([email]) + @@index([phone]) + @@index([lastName, firstName]) +} + +// ============================================================================= +// BOOKINGS & PAYMENTS +// ============================================================================= + +model Booking { + id String @id @default(cuid()) + siteId String + courtId String + clientId String? + createdById String? + startTime DateTime + endTime DateTime + status BookingStatus @default(PENDING) + paymentType PaymentType @default(CASH) + totalPrice Decimal @db.Decimal(10, 2) + paidAmount Decimal @default(0) @db.Decimal(10, 2) + notes String? + playerNames String[] @default([]) + isRecurring Boolean @default(false) + recurringId String? + cancelledAt DateTime? + cancelReason String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + site Site @relation(fields: [siteId], references: [id], onDelete: Cascade) + court Court @relation(fields: [courtId], references: [id], onDelete: Cascade) + client Client? @relation(fields: [clientId], references: [id], onDelete: SetNull) + createdBy User? @relation("BookingCreatedBy", fields: [createdById], references: [id], onDelete: SetNull) + payments Payment[] + match Match? + + @@index([siteId]) + @@index([courtId]) + @@index([clientId]) + @@index([startTime, endTime]) + @@index([status]) + @@index([recurringId]) +} + +model Payment { + id String @id @default(cuid()) + bookingId String? + clientId String? + saleId String? + membershipId String? + amount Decimal @db.Decimal(10, 2) + paymentType PaymentType + reference String? + notes String? + cashRegisterId String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + booking Booking? @relation(fields: [bookingId], references: [id], onDelete: SetNull) + client Client? @relation(fields: [clientId], references: [id], onDelete: SetNull) + sale Sale? @relation(fields: [saleId], references: [id], onDelete: SetNull) + membership Membership? @relation(fields: [membershipId], references: [id], onDelete: SetNull) + cashRegister CashRegister? @relation(fields: [cashRegisterId], references: [id], onDelete: SetNull) + + @@index([bookingId]) + @@index([clientId]) + @@index([saleId]) + @@index([membershipId]) + @@index([cashRegisterId]) + @@index([createdAt]) +} + +// ============================================================================= +// MEMBERSHIPS +// ============================================================================= + +model MembershipPlan { + id String @id @default(cuid()) + organizationId String + name String + description String? + price Decimal @db.Decimal(10, 2) + durationMonths Int + courtHours Int? + discountPercent Decimal? @db.Decimal(5, 2) + benefits String[] @default([]) + isActive Boolean @default(true) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) + memberships Membership[] + + @@index([organizationId]) + @@index([isActive]) +} + +model Membership { + id String @id @default(cuid()) + planId String + clientId String + startDate DateTime + endDate DateTime + status MembershipStatus @default(ACTIVE) + remainingHours Int? + autoRenew Boolean @default(false) + notes String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + plan MembershipPlan @relation(fields: [planId], references: [id], onDelete: Cascade) + client Client @relation(fields: [clientId], references: [id], onDelete: Cascade) + payments Payment[] + + @@index([planId]) + @@index([clientId]) + @@index([status]) + @@index([endDate]) +} + +// ============================================================================= +// PRODUCTS & SALES +// ============================================================================= + +model ProductCategory { + id String @id @default(cuid()) + organizationId String + name String + description String? + displayOrder Int @default(0) + isActive Boolean @default(true) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) + products Product[] + + @@unique([organizationId, name]) + @@index([organizationId]) +} + +model Product { + id String @id @default(cuid()) + organizationId String + categoryId String? + name String + description String? + sku String? + price Decimal @db.Decimal(10, 2) + costPrice Decimal? @db.Decimal(10, 2) + stock Int @default(0) + minStock Int @default(0) + trackStock Boolean @default(true) + image String? + isActive Boolean @default(true) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) + category ProductCategory? @relation(fields: [categoryId], references: [id], onDelete: SetNull) + saleItems SaleItem[] + + @@unique([organizationId, sku]) + @@index([organizationId]) + @@index([categoryId]) + @@index([name]) + @@index([isActive]) +} + +model Sale { + id String @id @default(cuid()) + clientId String? + createdById String? + cashRegisterId String? + subtotal Decimal @db.Decimal(10, 2) + discount Decimal @default(0) @db.Decimal(10, 2) + tax Decimal @default(0) @db.Decimal(10, 2) + total Decimal @db.Decimal(10, 2) + paymentType PaymentType + notes String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + client Client? @relation(fields: [clientId], references: [id], onDelete: SetNull) + createdBy User? @relation("SaleCreatedBy", fields: [createdById], references: [id], onDelete: SetNull) + cashRegister CashRegister? @relation(fields: [cashRegisterId], references: [id], onDelete: SetNull) + items SaleItem[] + payments Payment[] + + @@index([clientId]) + @@index([createdById]) + @@index([cashRegisterId]) + @@index([createdAt]) +} + +model SaleItem { + id String @id @default(cuid()) + saleId String + productId String + quantity Int + unitPrice Decimal @db.Decimal(10, 2) + subtotal Decimal @db.Decimal(10, 2) + createdAt DateTime @default(now()) + + sale Sale @relation(fields: [saleId], references: [id], onDelete: Cascade) + product Product @relation(fields: [productId], references: [id], onDelete: Cascade) + + @@index([saleId]) + @@index([productId]) +} + +model CashRegister { + id String @id @default(cuid()) + siteId String + userId String + openedAt DateTime @default(now()) + closedAt DateTime? + openingAmount Decimal @db.Decimal(10, 2) + closingAmount Decimal? @db.Decimal(10, 2) + expectedAmount Decimal? @db.Decimal(10, 2) + difference Decimal? @db.Decimal(10, 2) + notes String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + site Site @relation(fields: [siteId], references: [id], onDelete: Cascade) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + sales Sale[] + payments Payment[] + + @@index([siteId]) + @@index([userId]) + @@index([openedAt]) + @@index([closedAt]) +} + +// ============================================================================= +// TOURNAMENTS +// ============================================================================= + +model Tournament { + id String @id @default(cuid()) + organizationId String + siteId String? + name String + description String? + type TournamentType @default(AMERICANO) + status TournamentStatus @default(DRAFT) + startDate DateTime + endDate DateTime? + maxPlayers Int? + entryFee Decimal? @db.Decimal(10, 2) + prizePool Decimal? @db.Decimal(10, 2) + rules String? + settings Json @default("{}") + image String? + isPublic Boolean @default(true) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) + site Site? @relation(fields: [siteId], references: [id], onDelete: SetNull) + inscriptions TournamentInscription[] + matches Match[] + + @@index([organizationId]) + @@index([siteId]) + @@index([status]) + @@index([startDate]) + @@index([type]) +} + +model TournamentInscription { + id String @id @default(cuid()) + tournamentId String + clientId String + partnerId String? + teamName String? + seedNumber Int? + paidAmount Decimal @default(0) @db.Decimal(10, 2) + isPaid Boolean @default(false) + notes String? + registeredAt DateTime @default(now()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + tournament Tournament @relation(fields: [tournamentId], references: [id], onDelete: Cascade) + client Client @relation(fields: [clientId], references: [id], onDelete: Cascade) + + @@unique([tournamentId, clientId]) + @@index([tournamentId]) + @@index([clientId]) +} + +model Match { + id String @id @default(cuid()) + tournamentId String + bookingId String? @unique + courtId String? + round Int + position Int + scheduledAt DateTime? + startedAt DateTime? + completedAt DateTime? + status MatchStatus @default(SCHEDULED) + team1Players String[] @default([]) + team2Players String[] @default([]) + team1Score Int[] @default([]) + team2Score Int[] @default([]) + winnerId String? + notes String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + tournament Tournament @relation(fields: [tournamentId], references: [id], onDelete: Cascade) + booking Booking? @relation(fields: [bookingId], references: [id], onDelete: SetNull) + court Court? @relation(fields: [courtId], references: [id], onDelete: SetNull) + + @@index([tournamentId]) + @@index([courtId]) + @@index([status]) + @@index([round, position]) + @@index([scheduledAt]) +}