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