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

561 lines
16 KiB
Markdown

# 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*