From df7660f37d64fa4a4e3cfcb8055d798190afa9ff Mon Sep 17 00:00:00 2001 From: Ivan Date: Sun, 1 Feb 2026 07:43:22 +0000 Subject: [PATCH] feat(db): add seed script with demo data Add comprehensive seed script for testing the application with realistic demo data: - Organization "Padel Pro Demo" - 3 sites (Norte, Sur, Centro) with different schedules - 2 courts per site (standard and premium pricing) - Admin users (1 super admin + 3 site admins) - Product categories (Bebidas, Snacks, Equipamiento, Alquiler) - Sample products with pricing - 3 membership plans (Basico, Premium, VIP) - 5 sample clients with one Premium membership Co-Authored-By: Claude Opus 4.5 --- apps/web/package.json | 4 +- apps/web/prisma/seed.ts | 452 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 455 insertions(+), 1 deletion(-) create mode 100644 apps/web/prisma/seed.ts diff --git a/apps/web/package.json b/apps/web/package.json index be4d1f7..b58f998 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -10,7 +10,8 @@ "type-check": "tsc --noEmit", "db:generate": "prisma generate", "db:push": "prisma db push", - "db:studio": "prisma studio" + "db:studio": "prisma studio", + "db:seed": "tsx prisma/seed.ts" }, "dependencies": { "@padel-pro/shared": "*", @@ -42,6 +43,7 @@ "postcss": "^8.4.35", "prisma": "^5.10.0", "tailwindcss": "^3.4.1", + "tsx": "^4.7.0", "typescript": "^5.3.3" } } diff --git a/apps/web/prisma/seed.ts b/apps/web/prisma/seed.ts new file mode 100644 index 0000000..42f2cc9 --- /dev/null +++ b/apps/web/prisma/seed.ts @@ -0,0 +1,452 @@ +import { PrismaClient, UserRole, CourtType, CourtStatus, MembershipStatus } from '@prisma/client'; +import bcrypt from 'bcryptjs'; + +const prisma = new PrismaClient(); + +async function main() { + console.log('Starting seed...'); + console.log(''); + + // ============================================================================= + // CLEANUP - Delete existing data in correct order + // ============================================================================= + console.log('Cleaning up existing data...'); + + await prisma.payment.deleteMany(); + await prisma.saleItem.deleteMany(); + await prisma.sale.deleteMany(); + await prisma.cashRegister.deleteMany(); + await prisma.match.deleteMany(); + await prisma.tournamentInscription.deleteMany(); + await prisma.tournament.deleteMany(); + await prisma.booking.deleteMany(); + await prisma.membership.deleteMany(); + await prisma.membershipPlan.deleteMany(); + await prisma.product.deleteMany(); + await prisma.productCategory.deleteMany(); + await prisma.client.deleteMany(); + await prisma.user.deleteMany(); + await prisma.court.deleteMany(); + await prisma.site.deleteMany(); + await prisma.organization.deleteMany(); + + console.log('Cleanup complete.'); + console.log(''); + + // ============================================================================= + // ORGANIZATION + // ============================================================================= + console.log('Creating organization...'); + + const organization = await prisma.organization.create({ + data: { + name: 'Padel Pro Demo', + slug: 'padel-pro-demo', + settings: { + currency: 'MXN', + timezone: 'America/Mexico_City', + language: 'es', + }, + }, + }); + + console.log(` Created organization: ${organization.name}`); + console.log(''); + + // ============================================================================= + // SITES + // ============================================================================= + console.log('Creating sites...'); + + const sitesData = [ + { + name: 'Sede Norte', + slug: 'sede-norte', + address: 'Av. Universidad 1000, Col. Del Valle', + phone: '+52 55 1234 5678', + email: 'norte@padelpro.com', + timezone: 'America/Mexico_City', + openTime: '07:00', + closeTime: '23:00', + }, + { + name: 'Sede Sur', + slug: 'sede-sur', + address: 'Av. Insurgentes 2000, Col. Roma', + phone: '+52 55 2345 6789', + email: 'sur@padelpro.com', + timezone: 'America/Mexico_City', + openTime: '08:00', + closeTime: '22:00', + }, + { + name: 'Sede Centro', + slug: 'sede-centro', + address: 'Calle Reforma 500, Centro Historico', + phone: '+52 55 3456 7890', + email: 'centro@padelpro.com', + timezone: 'America/Mexico_City', + openTime: '06:00', + closeTime: '24:00', + }, + ]; + + const sites = await Promise.all( + sitesData.map(async (siteData) => { + const site = await prisma.site.create({ + data: { + organizationId: organization.id, + ...siteData, + }, + }); + console.log(` Created site: ${site.name}`); + return site; + }) + ); + + console.log(''); + + // ============================================================================= + // COURTS (2 per site) + // ============================================================================= + console.log('Creating courts...'); + + const courts: { id: string; name: string; siteId: string }[] = []; + + for (const site of sites) { + const courtData = [ + { + name: 'Cancha 1', + type: CourtType.INDOOR, + status: CourtStatus.AVAILABLE, + pricePerHour: 350, + description: 'Cancha techada con iluminacion LED', + features: ['Iluminacion LED', 'Techada', 'Cristal panoramico'], + displayOrder: 1, + }, + { + name: 'Cancha 2', + type: CourtType.INDOOR, + status: CourtStatus.AVAILABLE, + pricePerHour: 450, + description: 'Cancha premium con aire acondicionado', + features: ['Iluminacion LED', 'Techada', 'Aire acondicionado', 'Cristal panoramico', 'Premium'], + displayOrder: 2, + }, + ]; + + for (const court of courtData) { + const created = await prisma.court.create({ + data: { + siteId: site.id, + ...court, + }, + }); + courts.push(created); + console.log(` Created court: ${site.name} - ${created.name}`); + } + } + + console.log(''); + + // ============================================================================= + // ADMIN USER (SUPER_ADMIN) + // ============================================================================= + console.log('Creating admin users...'); + + const hashedPassword = await bcrypt.hash('admin123', 10); + + const adminUser = await prisma.user.create({ + data: { + organizationId: organization.id, + email: 'admin@padelpro.com', + password: hashedPassword, + firstName: 'Administrador', + lastName: 'Sistema', + role: UserRole.SUPER_ADMIN, + phone: '+52 55 9999 0000', + siteIds: sites.map(s => s.id), + }, + }); + + console.log(` Created super admin: ${adminUser.email}`); + + // ============================================================================= + // SITE ADMINS (one per site) + // ============================================================================= + const siteAdminsData = [ + { email: 'norte@padelpro.com', firstName: 'Carlos', lastName: 'Rodriguez', site: sites[0] }, + { email: 'sur@padelpro.com', firstName: 'Maria', lastName: 'Gonzalez', site: sites[1] }, + { email: 'centro@padelpro.com', firstName: 'Luis', lastName: 'Hernandez', site: sites[2] }, + ]; + + for (const adminData of siteAdminsData) { + const siteAdmin = await prisma.user.create({ + data: { + organizationId: organization.id, + email: adminData.email, + password: hashedPassword, + firstName: adminData.firstName, + lastName: adminData.lastName, + role: UserRole.SITE_ADMIN, + siteIds: [adminData.site.id], + }, + }); + + // Connect user to site + await prisma.site.update({ + where: { id: adminData.site.id }, + data: { + users: { + connect: { id: siteAdmin.id }, + }, + }, + }); + + console.log(` Created site admin: ${siteAdmin.email} (${adminData.site.name})`); + } + + console.log(''); + + // ============================================================================= + // PRODUCT CATEGORIES + // ============================================================================= + console.log('Creating product categories...'); + + const categoriesData = [ + { name: 'Bebidas', description: 'Bebidas y refrescos', displayOrder: 1 }, + { name: 'Snacks', description: 'Botanas y snacks', displayOrder: 2 }, + { name: 'Equipamiento', description: 'Equipo y accesorios de padel', displayOrder: 3 }, + { name: 'Alquiler', description: 'Articulos en renta', displayOrder: 4 }, + ]; + + const categories: { id: string; name: string }[] = []; + + for (const catData of categoriesData) { + const category = await prisma.productCategory.create({ + data: { + organizationId: organization.id, + ...catData, + }, + }); + categories.push(category); + console.log(` Created category: ${category.name}`); + } + + console.log(''); + + // ============================================================================= + // PRODUCTS (for organization, shown in Sede Norte initially) + // ============================================================================= + console.log('Creating products...'); + + const bebidasCategory = categories.find(c => c.name === 'Bebidas'); + const snacksCategory = categories.find(c => c.name === 'Snacks'); + const equipamientoCategory = categories.find(c => c.name === 'Equipamiento'); + const alquilerCategory = categories.find(c => c.name === 'Alquiler'); + + const productsData = [ + // Bebidas + { name: 'Agua', description: 'Agua natural 600ml', price: 20, costPrice: 8, stock: 100, categoryId: bebidasCategory?.id, sku: 'BEB-001' }, + { name: 'Gatorade', description: 'Bebida deportiva 500ml', price: 35, costPrice: 18, stock: 50, categoryId: bebidasCategory?.id, sku: 'BEB-002' }, + { name: 'Cerveza', description: 'Cerveza artesanal 355ml', price: 45, costPrice: 22, stock: 48, categoryId: bebidasCategory?.id, sku: 'BEB-003' }, + // Snacks + { name: 'Papas', description: 'Papas fritas 45g', price: 25, costPrice: 12, stock: 30, categoryId: snacksCategory?.id, sku: 'SNK-001' }, + { name: 'Barra energetica', description: 'Barra de proteina 50g', price: 30, costPrice: 15, stock: 25, categoryId: snacksCategory?.id, sku: 'SNK-002' }, + // Equipamiento + { name: 'Pelotas HEAD', description: 'Tubo de 3 pelotas HEAD Pro', price: 180, costPrice: 90, stock: 20, categoryId: equipamientoCategory?.id, sku: 'EQP-001' }, + { name: 'Grip', description: 'Overgrip Wilson Pro', price: 50, costPrice: 25, stock: 40, categoryId: equipamientoCategory?.id, sku: 'EQP-002' }, + // Alquiler + { name: 'Raqueta alquiler', description: 'Raqueta de padel (por hora)', price: 100, costPrice: 0, stock: 10, categoryId: alquilerCategory?.id, sku: 'ALQ-001', trackStock: false }, + ]; + + for (const productData of productsData) { + const product = await prisma.product.create({ + data: { + organizationId: organization.id, + ...productData, + }, + }); + console.log(` Created product: ${product.name} - $${product.price}`); + } + + console.log(''); + + // ============================================================================= + // MEMBERSHIP PLANS + // ============================================================================= + console.log('Creating membership plans...'); + + const membershipPlansData = [ + { + name: 'Basico', + description: 'Plan basico mensual con beneficios esenciales', + price: 499, + durationMonths: 1, + courtHours: 2, + discountPercent: 10, + benefits: ['2 horas gratis de cancha al mes', '10% descuento en reservas', '5% descuento en tienda'], + }, + { + name: 'Premium', + description: 'Plan premium con mayores beneficios', + price: 899, + durationMonths: 1, + courtHours: 5, + discountPercent: 20, + benefits: ['5 horas gratis de cancha al mes', '20% descuento en reservas', '10% descuento en tienda', 'Acceso prioritario a torneos'], + }, + { + name: 'VIP', + description: 'Plan VIP con todos los beneficios', + price: 1499, + durationMonths: 1, + courtHours: 10, + discountPercent: 30, + benefits: ['10 horas gratis de cancha al mes', '30% descuento en reservas', '15% descuento en tienda', 'Acceso prioritario a torneos', 'Invitados con descuento', 'Casillero incluido'], + }, + ]; + + const membershipPlans: { id: string; name: string; courtHours: number | null }[] = []; + + for (const planData of membershipPlansData) { + const plan = await prisma.membershipPlan.create({ + data: { + organizationId: organization.id, + ...planData, + }, + }); + membershipPlans.push(plan); + console.log(` Created membership plan: ${plan.name} - $${plan.price}/mes`); + } + + console.log(''); + + // ============================================================================= + // SAMPLE CLIENTS + // ============================================================================= + console.log('Creating sample clients...'); + + const clientsData = [ + { + firstName: 'Juan', + lastName: 'Perez', + email: 'juan.perez@email.com', + phone: '+52 55 1111 2222', + level: 'Intermedio', + preferredHand: 'Derecha', + }, + { + firstName: 'Maria', + lastName: 'Garcia', + email: 'maria.garcia@email.com', + phone: '+52 55 2222 3333', + level: 'Avanzado', + preferredHand: 'Derecha', + }, + { + firstName: 'Carlos', + lastName: 'Lopez', + email: 'carlos.lopez@email.com', + phone: '+52 55 3333 4444', + level: 'Principiante', + preferredHand: 'Izquierda', + }, + { + firstName: 'Ana', + lastName: 'Martinez', + email: 'ana.martinez@email.com', + phone: '+52 55 4444 5555', + level: 'Intermedio', + preferredHand: 'Derecha', + }, + { + firstName: 'Roberto', + lastName: 'Sanchez', + email: 'roberto.sanchez@email.com', + phone: '+52 55 5555 6666', + level: 'Avanzado', + preferredHand: 'Derecha', + }, + ]; + + const clients: { id: string; firstName: string; lastName: string }[] = []; + + for (const clientData of clientsData) { + const client = await prisma.client.create({ + data: { + organizationId: organization.id, + ...clientData, + }, + }); + clients.push(client); + console.log(` Created client: ${client.firstName} ${client.lastName}`); + } + + console.log(''); + + // ============================================================================= + // MEMBERSHIP FOR ONE CLIENT (Maria Garcia with Premium) + // ============================================================================= + console.log('Creating sample membership...'); + + const premiumPlan = membershipPlans.find(p => p.name === 'Premium'); + const mariaClient = clients.find(c => c.firstName === 'Maria'); + + if (premiumPlan && mariaClient) { + const startDate = new Date(); + const endDate = new Date(); + endDate.setMonth(endDate.getMonth() + 1); + + const membership = await prisma.membership.create({ + data: { + planId: premiumPlan.id, + clientId: mariaClient.id, + startDate, + endDate, + status: MembershipStatus.ACTIVE, + remainingHours: premiumPlan.courtHours, + autoRenew: true, + }, + }); + + console.log(` Created Premium membership for ${mariaClient.firstName} ${mariaClient.lastName}`); + } + + console.log(''); + + // ============================================================================= + // SUMMARY + // ============================================================================= + console.log('='.repeat(60)); + console.log('Seed completed successfully!'); + console.log('='.repeat(60)); + console.log(''); + console.log('Summary:'); + console.log(` - 1 Organization: ${organization.name}`); + console.log(` - ${sites.length} Sites`); + console.log(` - ${courts.length} Courts (${courts.length / sites.length} per site)`); + console.log(` - 4 Users (1 super admin + 3 site admins)`); + console.log(` - ${categories.length} Product Categories`); + console.log(` - ${productsData.length} Products`); + console.log(` - ${membershipPlans.length} Membership Plans`); + console.log(` - ${clients.length} Sample Clients`); + console.log(` - 1 Active Membership`); + console.log(''); + console.log('Login credentials:'); + console.log(' Super Admin: admin@padelpro.com / admin123'); + console.log(' Site Admins: norte@padelpro.com, sur@padelpro.com, centro@padelpro.com / admin123'); + console.log(''); +} + +main() + .then(async () => { + await prisma.$disconnect(); + }) + .catch(async (e) => { + console.error(''); + console.error('Error during seed:'); + console.error(e); + await prisma.$disconnect(); + process.exit(1); + });