docs: add Padel Pro system design document
Complete design specification including: - Multi-site architecture for padel club management - Booking system with client app and admin panel - Point of sale (POS) with inventory - Tournaments and leagues module - Membership plans with benefits - Full database schema (Prisma) - UI/UX guidelines and color palette - Project structure (monorepo with Turborepo) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
560
docs/plans/2026-02-01-padel-pro-design.md
Normal file
560
docs/plans/2026-02-01-padel-pro-design.md
Normal file
@@ -0,0 +1,560 @@
|
||||
# Padel Pro - 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
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────────────┐
|
||||
│ PADEL PRO - 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
|
||||
|
||||
```
|
||||
padel-pro/
|
||||
├── 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*
|
||||
Reference in New Issue
Block a user