Files
app-padel/docs/plans/2026-02-01-padel-pro-design.md
Ivan 45ceeba9e3 feat: rebrand application from Padel Pro to SmashPoint
Complete rename across all layers: UI branding, package names
(@smashpoint/web, @smashpoint/shared), infrastructure (Docker,
DB config), seed data, documentation, and logo/favicon.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 02:46:29 +00:00

16 KiB

SmashPoint - Documento de Diseño

Fecha: 2026-02-01 Estado: Aprobado Versión: 1.0


Resumen Ejecutivo

Sistema integral de gestión para cadena de clubes de pádel con múltiples sedes. Incluye gestión de reservas, punto de venta, torneos/ligas y membresías.

Alcance

Aspecto Decisión
Tipo de usuario Dueño de club/canchas
Escala Múltiples sedes
Modelo de negocio Mixto (reservas por hora + membresías)
Funcionalidades Reservas, Torneos/Ligas, Punto de Venta, Membresías
Pagos Efectivo, transferencias, terminal física
Plataformas Web (admin) + App móvil (clientes)

Arquitectura

Enfoque: Monolito Moderno

┌─────────────────────────────────────────┐
│           Next.js (Web + API)           │
│  ┌─────────┐ ┌─────────┐ ┌───────────┐  │
│  │ Admin   │ │ API     │ │ Auth      │  │
│  │ Panel   │ │ Routes  │ │ (NextAuth)│  │
│  └─────────┘ └─────────┘ └───────────┘  │
└─────────────────────────────────────────┘
         │                    │
         ▼                    ▼
┌─────────────────┐   ┌───────────────────┐
│   PostgreSQL    │   │  React Native App │
│   (Prisma ORM)  │   │   (Clientes)      │
└─────────────────┘   └───────────────────┘

Stack Técnico

Capa Tecnología
Frontend Web Next.js 14, TypeScript, Tailwind CSS, shadcn/ui, React Query
Backend/API Next.js API Routes, Prisma ORM, NextAuth.js, Zod
Base de datos PostgreSQL
Mobile React Native, Expo, TypeScript, NativeWind
Infraestructura Vercel/VPS, Railway/Supabase (DB), Cloudinary

Módulos del Sistema

┌────────────────────────────────────────────────────────────┐
│                    SMASHPOINT - MÓDULOS                      │
├──────────────┬──────────────┬──────────────┬───────────────┤
│   RESERVAS   │    TORNEOS   │     POS      │   MEMBRESÍAS  │
│              │              │              │               │
│ • Calendario │ • Crear      │ • Productos  │ • Planes      │
│ • Disponib.  │   torneos    │ • Ventas     │ • Beneficios  │
│ • Booking    │ • Brackets   │ • Caja       │ • Renovación  │
│ • Bloqueos   │ • Rankings   │ • Cortes     │ • Historial   │
└──────────────┴──────────────┴──────────────┴───────────────┘
                              │
              ┌───────────────┴───────────────┐
              │         CORE (Base)           │
              │  • Multi-sede                 │
              │  • Usuarios y roles           │
              │  • Clientes                   │
              │  • Pagos y caja               │
              │  • Reportes                   │
              └───────────────────────────────┘

Roles de Usuario

Rol Permisos
Super Admin Todas las sedes, reportes consolidados, configuración global
Admin de Sede Gestiona una sede específica, reservas, caja y personal
Recepcionista Opera reservas, cobra, registra pagos, atiende clientes
Cliente (App) Reserva canchas, ve partidos, inscribe a torneos

Modelo de Datos

Entidades Core

// Organización y Sedes
model Organization {
  id        String   @id @default(cuid())
  name      String
  logo      String?
  sites     Site[]
  users     User[]
  clients   Client[]
  createdAt DateTime @default(now())
}

model Site {
  id             String       @id @default(cuid())
  organizationId String
  organization   Organization @relation(fields: [organizationId], references: [id])
  name           String
  address        String
  phone          String?
  openTime       String       // "08:00"
  closeTime      String       // "22:00"
  courts         Court[]
  users          User[]
  products       Product[]
  tournaments    Tournament[]
  createdAt      DateTime     @default(now())
}

model Court {
  id           String        @id @default(cuid())
  siteId       String
  site         Site          @relation(fields: [siteId], references: [id])
  name         String
  type         CourtType     @default(DOUBLES)
  pricePerHour Decimal
  premiumPrice Decimal?      // Precio horario premium
  status       CourtStatus   @default(ACTIVE)
  bookings     Booking[]
}

enum CourtType {
  SINGLES
  DOUBLES
  MIXED
}

enum CourtStatus {
  ACTIVE
  MAINTENANCE
  INACTIVE
}

// Usuarios y Clientes
model User {
  id             String       @id @default(cuid())
  organizationId String
  organization   Organization @relation(fields: [organizationId], references: [id])
  siteId         String?
  site           Site?        @relation(fields: [siteId], references: [id])
  email          String       @unique
  password       String
  name           String
  phone          String?
  role           UserRole
  createdAt      DateTime     @default(now())
}

enum UserRole {
  SUPER_ADMIN
  SITE_ADMIN
  RECEPTIONIST
}

model Client {
  id             String        @id @default(cuid())
  organizationId String
  organization   Organization  @relation(fields: [organizationId], references: [id])
  email          String        @unique
  password       String
  name           String
  phone          String?
  photo          String?
  balance        Decimal       @default(0)
  membership     Membership?
  bookings       Booking[]
  createdAt      DateTime      @default(now())
}

// Reservas
model Booking {
  id          String        @id @default(cuid())
  courtId     String
  court       Court         @relation(fields: [courtId], references: [id])
  clientId    String
  client      Client        @relation(fields: [clientId], references: [id])
  date        DateTime
  startTime   String        // "10:00"
  endTime     String        // "11:00"
  price       Decimal
  status      BookingStatus @default(PENDING)
  paymentType PaymentType?
  isPaid      Boolean       @default(false)
  notes       String?
  payments    Payment[]
  createdAt   DateTime      @default(now())
  createdBy   String?       // User ID si fue creada por staff
}

enum BookingStatus {
  PENDING
  CONFIRMED
  CANCELLED
  COMPLETED
}

enum PaymentType {
  CASH
  TRANSFER
  CARD_TERMINAL
}

// Pagos
model Payment {
  id        String      @id @default(cuid())
  amount    Decimal
  method    PaymentType
  reference String?
  bookingId String?
  booking   Booking?    @relation(fields: [bookingId], references: [id])
  saleId    String?
  sale      Sale?       @relation(fields: [saleId], references: [id])
  createdBy String      // User ID
  createdAt DateTime    @default(now())
}

// Membresías
model MembershipPlan {
  id             String       @id @default(cuid())
  organizationId String
  name           String
  price          Decimal
  freeHours      Int
  bookingDiscount Int         // Porcentaje
  storeDiscount  Int          // Porcentaje
  extraBenefits  String?
  memberships    Membership[]
}

model Membership {
  id        String         @id @default(cuid())
  clientId  String         @unique
  client    Client         @relation(fields: [clientId], references: [id])
  planId    String
  plan      MembershipPlan @relation(fields: [planId], references: [id])
  startDate DateTime
  endDate   DateTime
  hoursUsed Int            @default(0)
  status    MembershipStatus @default(ACTIVE)
}

enum MembershipStatus {
  ACTIVE
  EXPIRED
  CANCELLED
}

// Punto de Venta
model ProductCategory {
  id       String    @id @default(cuid())
  name     String
  products Product[]
}

model Product {
  id         String          @id @default(cuid())
  siteId     String
  site       Site            @relation(fields: [siteId], references: [id])
  categoryId String
  category   ProductCategory @relation(fields: [categoryId], references: [id])
  name       String
  price      Decimal
  stock      Int             @default(0)
  minStock   Int             @default(5)
  saleItems  SaleItem[]
}

model Sale {
  id        String     @id @default(cuid())
  siteId    String
  items     SaleItem[]
  total     Decimal
  payments  Payment[]
  createdBy String
  createdAt DateTime   @default(now())
}

model SaleItem {
  id        String  @id @default(cuid())
  saleId    String
  sale      Sale    @relation(fields: [saleId], references: [id])
  productId String
  product   Product @relation(fields: [productId], references: [id])
  quantity  Int
  price     Decimal
}

// Control de Caja
model CashRegister {
  id           String   @id @default(cuid())
  siteId       String
  openedBy     String
  closedBy     String?
  openingAmount Decimal
  closingAmount Decimal?
  expectedAmount Decimal?
  openedAt     DateTime @default(now())
  closedAt     DateTime?
  status       CashRegisterStatus @default(OPEN)
}

enum CashRegisterStatus {
  OPEN
  CLOSED
}

// Torneos
model Tournament {
  id           String              @id @default(cuid())
  siteId       String
  site         Site                @relation(fields: [siteId], references: [id])
  name         String
  description  String?
  date         DateTime
  endDate      DateTime?
  type         TournamentType
  category     String?             // A, B, C o null si no aplica
  maxTeams     Int
  price        Decimal
  status       TournamentStatus    @default(DRAFT)
  inscriptions TournamentInscription[]
  matches      Match[]
}

enum TournamentType {
  SINGLE_ELIMINATION
  DOUBLE_ELIMINATION
  ROUND_ROBIN
  LEAGUE
}

enum TournamentStatus {
  DRAFT
  OPEN
  IN_PROGRESS
  FINISHED
  CANCELLED
}

model TournamentInscription {
  id           String     @id @default(cuid())
  tournamentId String
  tournament   Tournament @relation(fields: [tournamentId], references: [id])
  player1Id    String
  player2Id    String?    // Para parejas
  teamName     String?
  createdAt    DateTime   @default(now())
}

model Match {
  id           String     @id @default(cuid())
  tournamentId String
  tournament   Tournament @relation(fields: [tournamentId], references: [id])
  round        Int
  position     Int
  team1Id      String?
  team2Id      String?
  score1       String?
  score2       String?
  winnerId     String?
  courtId      String?
  scheduledAt  DateTime?
  status       MatchStatus @default(PENDING)
}

enum MatchStatus {
  PENDING
  IN_PROGRESS
  FINISHED
}

Flujos Principales

Flujo de Reserva - Cliente (App)

  1. Cliente abre app y selecciona sede
  2. Elige fecha en calendario
  3. Ve horarios disponibles con precios
  4. Selecciona horario y confirma
  5. Reserva queda como PENDIENTE
  6. Al llegar, staff cobra y marca como PAGADA

Flujo de Reserva - Recepcionista (Web)

  1. Ve calendario visual de canchas
  2. Click en slot disponible
  3. Busca cliente existente o crea nuevo
  4. Si cliente presente: cobra y confirma
  5. Si no presente: reserva pendiente

Flujo de Venta POS

  1. Recepcionista abre pantalla de caja
  2. Agrega productos al ticket
  3. Opcionalmente agrega reserva al mismo ticket
  4. Selecciona método de pago
  5. Registra pago y cierra venta

Flujo de Torneo

  1. Admin crea torneo (nombre, fecha, categoría, precio)
  2. Abre inscripciones
  3. Clientes se inscriben desde app
  4. Admin cierra inscripciones y genera bracket
  5. Asigna horarios y canchas
  6. Registra resultados de partidos
  7. Sistema avanza bracket automáticamente

Diseño Visual

Paleta de Colores

Uso Color Hex
Primario Azul profundo #1E3A5F
Acento Verde pádel #22C55E
Fondo Blanco/Gris claro #F8FAFC
Texto Gris oscuro #1E293B
Éxito Verde #22C55E
Alerta Ámbar #F59E0B
Error Rojo suave #EF4444

Principios de Diseño

  1. Minimalista: Solo información esencial en pantalla
  2. Touch-friendly: Botones grandes en móvil (mínimo 44px)
  3. Feedback visual: Confirmaciones claras, estados visibles
  4. Responsive: Admin funciona en desktop y tablet
  5. Modo oscuro: Opcional para ambas plataformas
  6. Consistencia: Mismos patrones en web y móvil

Componentes UI (shadcn/ui)

  • Calendario de reservas (vista día/semana)
  • Cards para estadísticas
  • Tablas con filtros y búsqueda
  • Modales para formularios
  • Toast notifications
  • Sidebar navegación colapsable

Estructura del Proyecto

smashpoint/
├── apps/
│   ├── web/                  # Next.js (Admin + API)
│   │   ├── app/
│   │   │   ├── (admin)/      # Panel administrativo
│   │   │   │   ├── dashboard/
│   │   │   │   ├── bookings/
│   │   │   │   ├── pos/
│   │   │   │   ├── tournaments/
│   │   │   │   ├── clients/
│   │   │   │   ├── memberships/
│   │   │   │   ├── reports/
│   │   │   │   └── settings/
│   │   │   ├── (auth)/
│   │   │   │   ├── login/
│   │   │   │   └── forgot-password/
│   │   │   └── api/
│   │   │       ├── auth/
│   │   │       ├── bookings/
│   │   │       ├── clients/
│   │   │       ├── courts/
│   │   │       ├── products/
│   │   │       ├── sales/
│   │   │       ├── tournaments/
│   │   │       └── reports/
│   │   ├── components/
│   │   │   ├── ui/           # shadcn components
│   │   │   ├── booking/
│   │   │   ├── pos/
│   │   │   └── layout/
│   │   └── lib/
│   │       ├── db.ts
│   │       ├── auth.ts
│   │       └── utils.ts
│   │
│   └── mobile/               # React Native (Clientes)
│       ├── app/              # Expo Router
│       │   ├── (tabs)/
│       │   │   ├── index.tsx      # Home/Reservar
│       │   │   ├── bookings.tsx   # Mis reservas
│       │   │   ├── tournaments.tsx
│       │   │   └── profile.tsx
│       │   ├── (auth)/
│       │   └── booking/[id].tsx
│       ├── components/
│       └── services/
│           └── api.ts
│
├── packages/
│   └── shared/
│       ├── types/
│       └── validations/
│
├── prisma/
│   ├── schema.prisma
│   └── seed.ts
│
├── docs/
│   └── plans/
│
├── turbo.json
├── package.json
└── README.md

Consideraciones de Seguridad

  • Autenticación JWT con refresh tokens
  • Passwords hasheados con bcrypt
  • Validación de inputs con Zod
  • Rate limiting en API
  • CORS configurado correctamente
  • Variables sensibles en environment

Próximos Pasos

  1. Inicializar monorepo con Turborepo
  2. Configurar base de datos y Prisma
  3. Implementar autenticación
  4. Desarrollar módulo de reservas (core)
  5. Agregar POS
  6. Implementar torneos
  7. Desarrollar app móvil
  8. Testing y QA
  9. Deploy a producción

Documento generado colaborativamente - Febrero 2026