# 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

```prisma
// 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*
