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>
16 KiB
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)
- Cliente abre app y selecciona sede
- Elige fecha en calendario
- Ve horarios disponibles con precios
- Selecciona horario y confirma
- Reserva queda como PENDIENTE
- Al llegar, staff cobra y marca como PAGADA
Flujo de Reserva - Recepcionista (Web)
- Ve calendario visual de canchas
- Click en slot disponible
- Busca cliente existente o crea nuevo
- Si cliente presente: cobra y confirma
- Si no presente: reserva pendiente
Flujo de Venta POS
- Recepcionista abre pantalla de caja
- Agrega productos al ticket
- Opcionalmente agrega reserva al mismo ticket
- Selecciona método de pago
- Registra pago y cierra venta
Flujo de Torneo
- Admin crea torneo (nombre, fecha, categoría, precio)
- Abre inscripciones
- Clientes se inscriben desde app
- Admin cierra inscripciones y genera bracket
- Asigna horarios y canchas
- Registra resultados de partidos
- 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
- Minimalista: Solo información esencial en pantalla
- Touch-friendly: Botones grandes en móvil (mínimo 44px)
- Feedback visual: Confirmaciones claras, estados visibles
- Responsive: Admin funciona en desktop y tablet
- Modo oscuro: Opcional para ambas plataformas
- 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
- Inicializar monorepo con Turborepo
- Configurar base de datos y Prisma
- Implementar autenticación
- Desarrollar módulo de reservas (core)
- Agregar POS
- Implementar torneos
- Desarrollar app móvil
- Testing y QA
- Deploy a producción
Documento generado colaborativamente - Febrero 2026