From f1f766a9c7ad12429cf75a94d46f93380ad53900 Mon Sep 17 00:00:00 2001 From: fullsizemalt <106900403+fullsizemalt@users.noreply.github.com> Date: Fri, 12 Dec 2025 20:08:05 -0800 Subject: [PATCH] feat: separate demo seed from base seed Base seed (npm run seed): - Just roles + admin user for clean onboarding Demo seed (npm run seed:demo): - Complete 2025 operation data - 5 demo staff members - 7 batches across all stages - Touch points history (50+ per batch) - 30 days walkthrough history with reservoir/irrigation/health checks - 11 SOPs and documents - Supply inventory - IPM schedules - Weight logs - Time punch history - Announcements --- backend/package.json | 7 +- backend/prisma/seed-demo.js | 395 ++++++++++++++++++++++++++++++++++++ backend/prisma/seed.js | 334 ++---------------------------- 3 files changed, 418 insertions(+), 318 deletions(-) create mode 100644 backend/prisma/seed-demo.js diff --git a/backend/package.json b/backend/package.json index ce14efc..19f68d7 100644 --- a/backend/package.json +++ b/backend/package.json @@ -7,7 +7,10 @@ "start": "node dist/server.js", "dev": "ts-node-dev --transpile-only src/server.ts", "test": "jest", - "lint": "eslint src/**/*.ts" + "lint": "eslint src/**/*.ts", + "seed": "npx prisma db seed", + "seed:demo": "node prisma/seed-demo.js", + "seed:all": "npx prisma db seed && node prisma/seed-demo.js" }, "prisma": { "seed": "node prisma/seed.js" @@ -37,4 +40,4 @@ "ts-node-dev": "^2.0.0", "typescript": "^5.3.3" } -} +} \ No newline at end of file diff --git a/backend/prisma/seed-demo.js b/backend/prisma/seed-demo.js new file mode 100644 index 0000000..ecffda9 --- /dev/null +++ b/backend/prisma/seed-demo.js @@ -0,0 +1,395 @@ +/** + * DEMO DATA SEED - 777 Wolfpack 2025 Operation + * + * Run after base seed to populate with realistic demo data + * showing a successful grow operation throughout 2025. + * + * Usage: npm run seed:demo + */ + +const { PrismaClient, RoomType } = require('@prisma/client'); +const bcrypt = require('bcryptjs'); + +const prisma = new PrismaClient(); +const DEMO_PREFIX = '[DEMO]'; + +// Helper: days ago +const daysAgo = (days) => new Date(Date.now() - days * 24 * 60 * 60 * 1000); +const hoursAgo = (hours) => new Date(Date.now() - hours * 60 * 60 * 1000); + +async function hashPassword(password) { + return bcrypt.hash(password, 10); +} + +async function main() { + console.log('๐ŸŒฟ Seeding 2025 Demo Operation Data...\n'); + + // Get roles + const managerRole = await prisma.role.findUnique({ where: { name: 'Manager' } }); + const growerRole = await prisma.role.findUnique({ where: { name: 'Grower' } }); + const workerRole = await prisma.role.findUnique({ where: { name: 'Worker' } }); + + // ==================== DEMO USERS ==================== + console.log('๐Ÿ‘ฅ Creating Demo Staff...'); + const demoUsers = [ + { email: 'sarah@demo.local', name: `${DEMO_PREFIX} Sarah Chen`, role: 'MANAGER', roleId: managerRole?.id, rate: 45 }, + { email: 'mike@demo.local', name: `${DEMO_PREFIX} Mike Thompson`, role: 'GROWER', roleId: growerRole?.id, rate: 28 }, + { email: 'alex@demo.local', name: `${DEMO_PREFIX} Alex Rivera`, role: 'STAFF', roleId: workerRole?.id, rate: 22 }, + { email: 'jordan@demo.local', name: `${DEMO_PREFIX} Jordan Lee`, role: 'GROWER', roleId: growerRole?.id, rate: 26 }, + { email: 'sam@demo.local', name: `${DEMO_PREFIX} Sam Martinez`, role: 'STAFF', roleId: workerRole?.id, rate: 20 }, + ]; + + const users = {}; + for (const u of demoUsers) { + let user = await prisma.user.findUnique({ where: { email: u.email } }); + if (!user) { + user = await prisma.user.create({ + data: { ...u, passwordHash: await hashPassword('demo1234') } + }); + console.log(` โœ“ ${u.name}`); + } + users[u.email.split('@')[0]] = user; + } + + // ==================== ROOMS ==================== + console.log('\n๐Ÿ  Creating Grow Rooms...'); + const roomsData = [ + { name: `${DEMO_PREFIX} Veg Room 1`, type: RoomType.VEG, sqft: 1200, capacity: 500, targetTemp: 78, targetHumidity: 65 }, + { name: `${DEMO_PREFIX} Veg Room 2`, type: RoomType.VEG, sqft: 1000, capacity: 400, targetTemp: 78, targetHumidity: 65 }, + { name: `${DEMO_PREFIX} Flower Room A`, type: RoomType.FLOWER, sqft: 2500, capacity: 300, targetTemp: 75, targetHumidity: 50 }, + { name: `${DEMO_PREFIX} Flower Room B`, type: RoomType.FLOWER, sqft: 2500, capacity: 300, targetTemp: 75, targetHumidity: 50 }, + { name: `${DEMO_PREFIX} Flower Room C`, type: RoomType.FLOWER, sqft: 2000, capacity: 250, targetTemp: 75, targetHumidity: 50 }, + { name: `${DEMO_PREFIX} Dry Room`, type: RoomType.DRY, sqft: 800, capacity: 100, targetTemp: 60, targetHumidity: 55 }, + { name: `${DEMO_PREFIX} Cure Room`, type: RoomType.CURE, sqft: 600, capacity: 50, targetTemp: 62, targetHumidity: 60 }, + { name: `${DEMO_PREFIX} Mother Room`, type: RoomType.MOTHER, sqft: 400, capacity: 50, targetTemp: 76, targetHumidity: 60 }, + ]; + + const rooms = {}; + for (const r of roomsData) { + let room = await prisma.room.findFirst({ where: { name: r.name } }); + if (!room) { + room = await prisma.room.create({ data: r }); + console.log(` โœ“ ${r.name}`); + } + rooms[r.name.replace(DEMO_PREFIX + ' ', '')] = room; + } + + // ==================== BATCHES ==================== + console.log('\n๐ŸŒฑ Creating Batches...'); + const batchesData = [ + { name: `${DEMO_PREFIX} Gorilla Glue #4 - B001`, strain: 'Gorilla Glue #4', stage: 'FLOWERING', plantCount: 280, startDate: daysAgo(52), room: 'Flower Room A' }, + { name: `${DEMO_PREFIX} Blue Dream - B002`, strain: 'Blue Dream', stage: 'VEGETATIVE', plantCount: 320, startDate: daysAgo(18), room: 'Veg Room 1' }, + { name: `${DEMO_PREFIX} Wedding Cake - B003`, strain: 'Wedding Cake', stage: 'FLOWERING', plantCount: 250, startDate: daysAgo(38), room: 'Flower Room B' }, + { name: `${DEMO_PREFIX} Purple Punch - B004`, strain: 'Purple Punch', stage: 'DRYING', plantCount: 40, startDate: daysAgo(78), room: 'Dry Room' }, + { name: `${DEMO_PREFIX} Gelato #41 - B005`, strain: 'Gelato #41', stage: 'CLONE_IN', plantCount: 100, startDate: daysAgo(3), room: 'Veg Room 2' }, + { name: `${DEMO_PREFIX} OG Kush - B006`, strain: 'OG Kush', stage: 'CURING', plantCount: 35, startDate: daysAgo(95), room: 'Cure Room' }, + { name: `${DEMO_PREFIX} Zkittlez - B007`, strain: 'Zkittlez', stage: 'FLOWERING', plantCount: 200, startDate: daysAgo(45), room: 'Flower Room C' }, + ]; + + const batches = {}; + for (const b of batchesData) { + let batch = await prisma.batch.findFirst({ where: { name: b.name } }); + if (!batch) { + batch = await prisma.batch.create({ + data: { + name: b.name, + strain: b.strain, + stage: b.stage, + plantCount: b.plantCount, + startDate: b.startDate, + status: 'ACTIVE', + source: 'CLONE', + roomId: rooms[b.room]?.id + } + }); + console.log(` โœ“ ${b.name} (${b.stage})`); + } + batches[b.strain] = batch; + } + + // ==================== TOUCH POINTS (Activity History) ==================== + console.log('\n๐Ÿ“‹ Creating Touch Point History...'); + const touchPointTypes = ['WATER', 'FEED', 'INSPECT', 'DEFOLIATE', 'TOP', 'TRANSPLANT', 'IPM']; + const userList = Object.values(users); + + for (const batch of Object.values(batches)) { + const batchAge = Math.floor((Date.now() - new Date(batch.startDate).getTime()) / 86400000); + const numTouchPoints = Math.min(batchAge * 2, 50); // ~2 per day, max 50 + + for (let i = 0; i < numTouchPoints; i++) { + const type = touchPointTypes[Math.floor(Math.random() * touchPointTypes.length)]; + const daysBack = Math.floor(Math.random() * batchAge); + const user = userList[Math.floor(Math.random() * userList.length)]; + + const notes = { + WATER: ['Standard feed', 'Light watering', 'Heavy watering - dry pots', 'pH 6.2'], + FEED: ['Week 3 flower nutes', 'Veg formula A+B', 'Full strength', 'Half strength flush'], + INSPECT: ['Looking healthy', 'Minor yellowing on lower leaves', 'Strong growth', 'Ready for transplant', 'Trichomes cloudy'], + DEFOLIATE: ['Removed lower fan leaves', 'Light lollipop', 'Heavy defoliation day 21'], + TOP: ['Topped to 4 nodes', 'FIMed'], + TRANSPLANT: ['1gal to 3gal', '3gal to 5gal'], + IPM: ['Pyganic spray', 'Neem foliar', 'Beneficial insects released', 'Dr Zymes application'], + }; + + await prisma.plantTouchPoint.create({ + data: { + batchId: batch.id, + userId: user.id, + type, + notes: notes[type][Math.floor(Math.random() * notes[type].length)], + createdAt: daysAgo(daysBack) + } + }); + } + console.log(` โœ“ ${batch.name}: ${numTouchPoints} touch points`); + } + + // ==================== DAILY WALKTHROUGHS ==================== + console.log('\n๐Ÿ“ Creating Walkthrough History...'); + for (let d = 0; d < 30; d++) { + const user = userList[Math.floor(Math.random() * userList.length)]; + const walkthrough = await prisma.dailyWalkthrough.create({ + data: { + userId: user.id, + status: 'COMPLETED', + startedAt: daysAgo(d), + completedAt: daysAgo(d), + notes: d === 0 ? null : [ + 'All rooms looking good', + 'Found spider mites in Flower A, treated with Pyganic', + 'VPD running high, adjusted humidity', + 'Reservoir topped off', + 'Noticed some cal-mag deficiency in veg', + null + ][Math.floor(Math.random() * 6)] + } + }); + + // Add reservoir checks + for (const tank of ['Veg Tank 1', 'Veg Tank 2', 'Flower Tank 1', 'Flower Tank 2']) { + await prisma.reservoirCheck.create({ + data: { + walkthroughId: walkthrough.id, + tankName: tank, + tankType: tank.includes('Veg') ? 'VEG' : 'FLOWER', + levelPercent: 60 + Math.floor(Math.random() * 40), + status: Math.random() > 0.1 ? 'OK' : 'LOW' + } + }); + } + + // Add irrigation checks + for (const zone of ['Veg Upstairs', 'Veg Downstairs', 'Flower Upstairs', 'Flower Downstairs']) { + const total = zone.includes('Veg') ? 48 : 64; + await prisma.irrigationCheck.create({ + data: { + walkthroughId: walkthrough.id, + zoneName: zone, + drippersTotal: total, + drippersWorking: total - Math.floor(Math.random() * 3), + waterFlow: Math.random() > 0.05, + nutrientsMixed: Math.random() > 0.02, + scheduleActive: true + } + }); + } + + // Add plant health checks + for (const zone of ['Veg Upstairs', 'Veg Downstairs', 'Flower Upstairs', 'Flower Downstairs']) { + await prisma.plantHealthCheck.create({ + data: { + walkthroughId: walkthrough.id, + zoneName: zone, + healthStatus: Math.random() > 0.15 ? 'GOOD' : Math.random() > 0.5 ? 'FAIR' : 'NEEDS_ATTENTION', + pestsObserved: Math.random() > 0.92, + waterAccess: 'OK', + foodAccess: 'OK', + flaggedForAttention: Math.random() > 0.9 + } + }); + } + } + console.log(' โœ“ 30 days of walkthrough history'); + + // ==================== DOCUMENTS (SOPs) ==================== + console.log('\n๐Ÿ“„ Creating SOPs & Documents...'); + const documents = [ + { name: `${DEMO_PREFIX} SOP - Daily Walkthrough Protocol`, category: 'SOP', description: 'Step-by-step daily inspection procedure' }, + { name: `${DEMO_PREFIX} SOP - IPM Treatment Schedule`, category: 'SOP', description: 'Integrated pest management protocols' }, + { name: `${DEMO_PREFIX} SOP - Nutrient Mixing (Front Row Ag)`, category: 'SOP', description: 'Nutrient stock preparation guide' }, + { name: `${DEMO_PREFIX} SOP - Clone Processing`, category: 'SOP', description: 'Receiving and processing clones' }, + { name: `${DEMO_PREFIX} SOP - Harvest Procedure`, category: 'SOP', description: 'Harvest, dry, and cure workflow' }, + { name: `${DEMO_PREFIX} SOP - Visitor Check-In`, category: 'SOP', description: 'Visitor management and NDA process' }, + { name: `${DEMO_PREFIX} SOP - Emergency Procedures`, category: 'SOP', description: 'Fire, flood, and security protocols' }, + { name: `${DEMO_PREFIX} Strain Library - Gorilla Glue #4`, category: 'STRAIN', description: 'Grow notes and specs for GG4' }, + { name: `${DEMO_PREFIX} Strain Library - Wedding Cake`, category: 'STRAIN', description: 'Grow notes and specs for Wedding Cake' }, + { name: `${DEMO_PREFIX} Equipment Manual - HVAC System`, category: 'MANUAL', description: 'HVAC maintenance and troubleshooting' }, + { name: `${DEMO_PREFIX} Compliance - METRC Training Guide`, category: 'COMPLIANCE', description: 'Track and trace procedures' }, + ]; + + for (const doc of documents) { + const existing = await prisma.document.findFirst({ where: { name: doc.name } }); + if (!existing) { + await prisma.document.create({ + data: { + ...doc, + status: 'PUBLISHED', + version: 1, + createdById: users.sarah?.id + } + }); + console.log(` โœ“ ${doc.name}`); + } + } + + // ==================== SUPPLIES ==================== + console.log('\n๐Ÿ“ฆ Creating Supplies Inventory...'); + const supplies = [ + { name: `${DEMO_PREFIX} Nitrile Gloves (L)`, category: 'PPE', quantity: 5, minThreshold: 10, unit: 'box', location: 'Ante Room', vendor: 'Uline' }, + { name: `${DEMO_PREFIX} Nitrile Gloves (M)`, category: 'PPE', quantity: 8, minThreshold: 10, unit: 'box', location: 'Ante Room', vendor: 'Uline' }, + { name: `${DEMO_PREFIX} Tyvek Suits`, category: 'PPE', quantity: 25, minThreshold: 20, unit: 'each', location: 'Ante Room', vendor: 'Uline' }, + { name: `${DEMO_PREFIX} Hypochlorous Acid`, category: 'CLEANING', quantity: 12, minThreshold: 5, unit: 'gallon', location: 'Janitor Closet', vendor: 'Athena' }, + { name: `${DEMO_PREFIX} Isopropyl Alcohol 99%`, category: 'CLEANING', quantity: 6, minThreshold: 4, unit: 'gallon', location: 'Trim Room', vendor: 'GrowGen' }, + { name: `${DEMO_PREFIX} Rockwool Cubes 4"`, category: 'OTHER', quantity: 450, minThreshold: 200, unit: 'cube', location: 'Veg Storage', vendor: 'GrowGen' }, + { name: `${DEMO_PREFIX} Coco Coir`, category: 'OTHER', quantity: 15, minThreshold: 10, unit: 'bag', location: 'Veg Storage', vendor: 'Botanicare' }, + { name: `${DEMO_PREFIX} Trimmers (Fiskars)`, category: 'MAINTENANCE', quantity: 12, minThreshold: 15, unit: 'pair', location: 'Trim Room', vendor: 'Amazon' }, + { name: `${DEMO_PREFIX} Front Row Ag - Part A`, category: 'NUTRIENTS', quantity: 40, minThreshold: 10, unit: 'bag (25lb)', location: 'Nutrient Storage', vendor: 'Front Row Ag' }, + { name: `${DEMO_PREFIX} Front Row Ag - Part B`, category: 'NUTRIENTS', quantity: 30, minThreshold: 10, unit: 'bag (25lb)', location: 'Nutrient Storage', vendor: 'Front Row Ag' }, + { name: `${DEMO_PREFIX} Front Row Ag - Bloom`, category: 'NUTRIENTS', quantity: 30, minThreshold: 10, unit: 'bag (25lb)', location: 'Nutrient Storage', vendor: 'Front Row Ag' }, + ]; + + for (const s of supplies) { + const existing = await prisma.supplyItem.findFirst({ where: { name: s.name } }); + if (!existing) { + await prisma.supplyItem.create({ data: s }); + } + } + console.log(` โœ“ ${supplies.length} supply items`); + + // ==================== TASKS ==================== + console.log('\nโœ… Creating Tasks...'); + const tasks = [ + { title: `${DEMO_PREFIX} Morning Walkthrough`, status: 'PENDING', priority: 'HIGH', dueDate: new Date() }, + { title: `${DEMO_PREFIX} IPM Treatment - Flower A`, status: 'IN_PROGRESS', priority: 'HIGH', dueDate: new Date() }, + { title: `${DEMO_PREFIX} Trim Batch 004`, status: 'PENDING', priority: 'MEDIUM', dueDate: daysAgo(-1) }, + { title: `${DEMO_PREFIX} Inventory Audit - PPE`, status: 'PENDING', priority: 'LOW', dueDate: daysAgo(-3) }, + { title: `${DEMO_PREFIX} HVAC Filter Change`, status: 'COMPLETED', priority: 'MEDIUM', dueDate: daysAgo(2) }, + { title: `${DEMO_PREFIX} Clone Reception - Gelato`, status: 'COMPLETED', priority: 'HIGH', dueDate: daysAgo(3) }, + { title: `${DEMO_PREFIX} Transplant Blue Dream`, status: 'PENDING', priority: 'HIGH', dueDate: daysAgo(-2) }, + ]; + + for (const t of tasks) { + const existing = await prisma.task.findFirst({ where: { title: t.title } }); + if (!existing) { + await prisma.task.create({ + data: { ...t, assignedToId: users.mike?.id } + }); + } + } + console.log(` โœ“ ${tasks.length} tasks`); + + // ==================== IPM SCHEDULES ==================== + console.log('\n๐Ÿ›ก๏ธ Creating IPM Schedules...'); + for (const batch of Object.values(batches)) { + if (['VEGETATIVE', 'FLOWERING'].includes(batch.stage)) { + const existing = await prisma.iPMSchedule.findFirst({ where: { batchId: batch.id } }); + if (!existing) { + await prisma.iPMSchedule.create({ + data: { + batchId: batch.id, + product: ['Pyganic 5.0', 'Dr Zymes', 'Regalia', 'Venerate'][Math.floor(Math.random() * 4)], + intervalDays: 7 + Math.floor(Math.random() * 7), + nextTreatment: daysAgo(-Math.floor(Math.random() * 5)) + } + }); + } + } + } + console.log(' โœ“ IPM schedules for active batches'); + + // ==================== WEIGHT LOGS ==================== + console.log('\nโš–๏ธ Creating Weight Logs...'); + const curingBatch = Object.values(batches).find(b => b.stage === 'CURING'); + const dryingBatch = Object.values(batches).find(b => b.stage === 'DRYING'); + + if (curingBatch) { + await prisma.weightLog.create({ + data: { + batchId: curingBatch.id, + userId: users.mike?.id, + type: 'DRY', + weightGrams: 8450, + notes: 'Final dry weight before cure' + } + }); + } + if (dryingBatch) { + await prisma.weightLog.create({ + data: { + batchId: dryingBatch.id, + userId: users.jordan?.id, + type: 'WET', + weightGrams: 42000, + notes: 'Wet weight at harvest' + } + }); + } + console.log(' โœ“ Weight logs for dried batches'); + + // ==================== ANNOUNCEMENTS ==================== + console.log('\n๐Ÿ“ข Creating Announcements...'); + const announcements = [ + { title: `${DEMO_PREFIX} Welcome to 777 Wolfpack`, body: 'Demo environment with sample 2025 operation data.', priority: 'INFO' }, + { title: `${DEMO_PREFIX} Scheduled Inspection - Dec 15`, body: 'State inspector visit scheduled. Ensure compliance docs ready.', priority: 'WARNING', requiresAck: true }, + { title: `${DEMO_PREFIX} New IPM Protocol Effective`, body: 'Updated IPM schedule now in effect. Check SOP for details.', priority: 'INFO' }, + ]; + + for (const a of announcements) { + const existing = await prisma.announcement.findFirst({ where: { title: a.title } }); + if (!existing) { + await prisma.announcement.create({ + data: { ...a, createdById: users.sarah?.id } + }); + } + } + console.log(` โœ“ ${announcements.length} announcements`); + + // ==================== TIME LOGS ==================== + console.log('\nโฐ Creating Time Punch History...'); + for (const user of userList) { + for (let d = 1; d <= 14; d++) { + if (Math.random() > 0.15) { // 85% attendance + const clockIn = new Date(daysAgo(d)); + clockIn.setHours(7 + Math.floor(Math.random() * 2), Math.floor(Math.random() * 30), 0); + const clockOut = new Date(clockIn); + clockOut.setHours(clockIn.getHours() + 8 + Math.floor(Math.random() * 2)); + + await prisma.timeLog.create({ + data: { + userId: user.id, + clockIn, + clockOut, + notes: Math.random() > 0.8 ? 'Overtime for trim' : null + } + }); + } + } + } + console.log(' โœ“ 14 days of time punch history'); + + console.log('\nโœจ Demo seeding complete!'); + console.log('\nDemo Logins:'); + console.log(' Owner: admin@runfoo.run / password123'); + console.log(' Manager: sarah@demo.local / demo1234'); + console.log(' Grower: mike@demo.local / demo1234'); + console.log(' Worker: alex@demo.local / demo1234'); +} + +main() + .catch((e) => { + console.error('Demo seeding failed:', e); + process.exit(1); + }) + .finally(async () => { + await prisma.$disconnect(); + }); diff --git a/backend/prisma/seed.js b/backend/prisma/seed.js index e2c3d44..8ae09ac 100644 --- a/backend/prisma/seed.js +++ b/backend/prisma/seed.js @@ -3,17 +3,14 @@ const bcrypt = require('bcryptjs'); const prisma = new PrismaClient(); -// Demo data marker - all demo entities use this prefix for easy identification -const DEMO_PREFIX = '[DEMO]'; - async function hashPassword(password) { return bcrypt.hash(password, 10); } async function main() { - console.log('๐ŸŒฑ Seeding database with demo data...\n'); + console.log('๐ŸŒฑ Seeding database with base configuration...\n'); - // ==================== ROLES ==================== + // ==================== ROLES (Required) ==================== console.log('๐Ÿ“‹ Creating Roles...'); const rolesData = [ { @@ -75,323 +72,28 @@ async function main() { } const ownerRole = await prisma.role.findUnique({ where: { name: 'Facility Owner' } }); - const managerRole = await prisma.role.findUnique({ where: { name: 'Manager' } }); - const cultivatorRole = await prisma.role.findUnique({ where: { name: 'Grower' } }); - const workerRole = await prisma.role.findUnique({ where: { name: 'Worker' } }); - // ==================== USERS ==================== - console.log('\n๐Ÿ‘ฅ Creating Demo Users...'); - const usersData = [ - { - email: 'admin@runfoo.run', - password: 'password123', - name: 'Travis (Owner)', - role: 'OWNER', - roleId: ownerRole?.id, - rate: 100.00 - }, - { - email: 'manager@demo.local', - password: 'demo1234', - name: `${DEMO_PREFIX} Sarah Chen`, - role: 'MANAGER', - roleId: managerRole?.id, - rate: 45.00 - }, - { - email: 'cultivator@demo.local', - password: 'demo1234', - name: `${DEMO_PREFIX} Mike Thompson`, - role: 'GROWER', - roleId: cultivatorRole?.id, - rate: 28.00 - }, - { - email: 'worker@demo.local', - password: 'demo1234', - name: `${DEMO_PREFIX} Alex Rivera`, - role: 'STAFF', - roleId: workerRole?.id, - rate: 22.00 - } - ]; - - for (const u of usersData) { - const existing = await prisma.user.findUnique({ where: { email: u.email } }); - if (!existing) { - const passwordHash = await hashPassword(u.password); - await prisma.user.create({ - data: { - email: u.email, - passwordHash, - name: u.name, - role: u.role, - roleId: u.roleId, - rate: u.rate - } - }); - console.log(` โœ“ Created User: ${u.name} (${u.email})`); - } - } - - // ==================== ROOMS ==================== - console.log('\n๐Ÿ  Creating Demo Rooms...'); - const rooms = [ - { name: `${DEMO_PREFIX} Veg Room 1`, type: RoomType.VEG, sqft: 1200, capacity: 500, targetTemp: 78, targetHumidity: 65 }, - { name: `${DEMO_PREFIX} Veg Room 2`, type: RoomType.VEG, sqft: 1000, capacity: 400, targetTemp: 78, targetHumidity: 65 }, - { name: `${DEMO_PREFIX} Flower Room A`, type: RoomType.FLOWER, sqft: 2500, capacity: 300, targetTemp: 75, targetHumidity: 50 }, - { name: `${DEMO_PREFIX} Flower Room B`, type: RoomType.FLOWER, sqft: 2500, capacity: 300, targetTemp: 75, targetHumidity: 50 }, - { name: `${DEMO_PREFIX} Flower Room C`, type: RoomType.FLOWER, sqft: 2000, capacity: 250, targetTemp: 75, targetHumidity: 50 }, - { name: `${DEMO_PREFIX} Dry Room`, type: RoomType.DRY, sqft: 800, capacity: 100, targetTemp: 60, targetHumidity: 55 }, - { name: `${DEMO_PREFIX} Cure Room`, type: RoomType.CURE, sqft: 600, capacity: 50, targetTemp: 62, targetHumidity: 60 }, - { name: `${DEMO_PREFIX} Mother Room`, type: RoomType.MOTHER, sqft: 400, capacity: 50, targetTemp: 76, targetHumidity: 60 }, - ]; - - const createdRooms = []; - for (const r of rooms) { - let room = await prisma.room.findFirst({ where: { name: r.name } }); - if (!room) { - room = await prisma.room.create({ data: r }); - console.log(` โœ“ Created Room: ${r.name}`); - } - createdRooms.push(room); - } - - // ==================== BATCHES ==================== - console.log('\n๐ŸŒฑ Creating Demo Batches...'); - const batchesData = [ - { - name: `${DEMO_PREFIX} Gorilla Glue #4 - Batch 001`, - strain: 'Gorilla Glue #4', - status: 'ACTIVE', - stage: 'FLOWERING', - source: 'CLONE', - plantCount: 450, - startDate: new Date(Date.now() - 45 * 24 * 60 * 60 * 1000), // 45 days ago - roomId: createdRooms.find(r => r.name.includes('Flower Room A'))?.id - }, - { - name: `${DEMO_PREFIX} Blue Dream - Batch 002`, - strain: 'Blue Dream', - status: 'ACTIVE', - stage: 'VEGETATIVE', - source: 'CLONE', - plantCount: 300, - startDate: new Date(Date.now() - 20 * 24 * 60 * 60 * 1000), // 20 days ago - roomId: createdRooms.find(r => r.name.includes('Veg Room 1'))?.id - }, - { - name: `${DEMO_PREFIX} Wedding Cake - Batch 003`, - strain: 'Wedding Cake', - status: 'ACTIVE', - stage: 'FLOWERING', - source: 'CLONE', - plantCount: 400, - startDate: new Date(Date.now() - 10 * 24 * 60 * 60 * 1000), // 10 days ago (Just flipped) - roomId: createdRooms.find(r => r.name.includes('Flower Room B'))?.id - }, - { - name: `${DEMO_PREFIX} Purple Punch - Batch 004`, - strain: 'Purple Punch', - status: 'ACTIVE', - stage: 'DRYING', - source: 'CLONE', - plantCount: 30, - startDate: new Date(Date.now() - 75 * 24 * 60 * 60 * 1000), // 75 days ago - roomId: createdRooms.find(r => r.name.includes('Dry Room'))?.id - }, - { - name: `${DEMO_PREFIX} Gelato - Batch 005`, - strain: 'Gelato #41', - status: 'ACTIVE', - stage: 'CLONE_IN', - source: 'CLONE', - plantCount: 60, - startDate: new Date(), // Today - roomId: createdRooms.find(r => r.name.includes('Veg Room 2'))?.id - } - ]; - - const createdBatches = []; - for (const b of batchesData) { - let batch = await prisma.batch.findFirst({ where: { name: b.name } }); - if (!batch) { - batch = await prisma.batch.create({ data: b }); - console.log(` โœ“ Created Batch: ${b.name} (${b.stage})`); - } - createdBatches.push(batch); - } - - // ==================== IPM SCHEDULES ==================== - console.log('\n๐Ÿ›ก๏ธ Creating IPM Schedules...'); - for (const batch of createdBatches) { - if (batch.stage === 'VEGETATIVE' || batch.stage === 'FLOWERING') { - const existing = await prisma.iPMSchedule.findFirst({ where: { batchId: batch.id } }); - if (!existing) { - const nextTreatment = new Date(); - nextTreatment.setDate(nextTreatment.getDate() + Math.floor(Math.random() * 5) - 2); // -2 to +2 days - - await prisma.iPMSchedule.create({ - data: { - batchId: batch.id, - product: 'Pyganic 5.0', - intervalDays: 10, - nextTreatment - } - }); - console.log(` โœ“ Created IPM Schedule for: ${batch.name}`); - } - } - } - - // ==================== SUPPLIES ==================== - console.log('\n๐Ÿ“ฆ Creating Demo Supplies...'); - const supplies = [ - { name: `${DEMO_PREFIX} Nitrile Gloves (L)`, category: 'PPE', quantity: 5, minThreshold: 10, unit: 'box', location: 'Ante Room', vendor: 'Uline' }, - { name: `${DEMO_PREFIX} Nitrile Gloves (M)`, category: 'PPE', quantity: 8, minThreshold: 10, unit: 'box', location: 'Ante Room', vendor: 'Uline' }, - { name: `${DEMO_PREFIX} Tyvek Suits`, category: 'PPE', quantity: 25, minThreshold: 20, unit: 'each', location: 'Ante Room', vendor: 'Uline' }, - { name: `${DEMO_PREFIX} Hypochlorous Acid`, category: 'CLEANING', quantity: 12, minThreshold: 5, unit: 'gallon', location: 'Janitor Closet', vendor: 'Athena' }, - { name: `${DEMO_PREFIX} Isopropyl Alcohol 99%`, category: 'CLEANING', quantity: 6, minThreshold: 4, unit: 'gallon', location: 'Trim Room', vendor: 'GrowGen' }, - { name: `${DEMO_PREFIX} Rockwool Cubes 4"`, category: 'OTHER', quantity: 450, minThreshold: 200, unit: 'cube', location: 'Veg Storage', vendor: 'GrowGen' }, - { name: `${DEMO_PREFIX} Coco Coir`, category: 'OTHER', quantity: 15, minThreshold: 10, unit: 'bag', location: 'Veg Storage', vendor: 'Botanicare' }, - { name: `${DEMO_PREFIX} Trimmers (Fiskars)`, category: 'MAINTENANCE', quantity: 12, minThreshold: 15, unit: 'pair', location: 'Trim Room', vendor: 'Amazon' }, - { name: `${DEMO_PREFIX} Pruning Shears`, category: 'MAINTENANCE', quantity: 8, minThreshold: 6, unit: 'pair', location: 'Flower Room A', vendor: 'Amazon' }, - { name: `${DEMO_PREFIX} pH Test Kit`, category: 'MAINTENANCE', quantity: 3, minThreshold: 2, unit: 'kit', location: 'Nutrient Mix Station', vendor: 'Bluelab' }, - // Front Row Ag Nutrients - { name: `${DEMO_PREFIX} Front Row Ag - Part A`, category: 'NUTRIENTS', quantity: 40, minThreshold: 10, unit: 'bag (25lb)', location: 'Nutrient Storage', vendor: 'Front Row Ag' }, - { name: `${DEMO_PREFIX} Front Row Ag - Part B`, category: 'NUTRIENTS', quantity: 30, minThreshold: 10, unit: 'bag (25lb)', location: 'Nutrient Storage', vendor: 'Front Row Ag' }, - { name: `${DEMO_PREFIX} Front Row Ag - Bloom`, category: 'NUTRIENTS', quantity: 30, minThreshold: 10, unit: 'bag (25lb)', location: 'Nutrient Storage', vendor: 'Front Row Ag' }, - { name: `${DEMO_PREFIX} Front Row Ag - Phoszyme`, category: 'NUTRIENTS', quantity: 5, minThreshold: 2, unit: 'lb', location: 'Nutrient Storage', vendor: 'Front Row Ag' }, - ]; - - for (const s of supplies) { - const existing = await prisma.supplyItem.findFirst({ where: { name: s.name } }); - if (!existing) { - await prisma.supplyItem.create({ data: s }); - console.log(` โœ“ Created Supply: ${s.name} (qty: ${s.quantity})`); - } - } - - // ==================== TASKS ==================== - console.log('\nโœ… Creating Demo Tasks...'); - const manager = await prisma.user.findFirst({ where: { email: 'manager@demo.local' } }); - const cultivator = await prisma.user.findFirst({ where: { email: 'cultivator@demo.local' } }); - - const tasksData = [ - { - title: `${DEMO_PREFIX} Morning Walkthrough - Veg Rooms`, - description: 'Check all veg rooms for pest issues, water levels, and plant health', - status: 'PENDING', - priority: 'HIGH', - dueDate: new Date() - }, - { - title: `${DEMO_PREFIX} IPM Treatment - Flower Room A`, - description: 'Apply Pyganic 5.0 root drench as per schedule', - status: 'IN_PROGRESS', - priority: 'HIGH', - dueDate: new Date() - }, - { - title: `${DEMO_PREFIX} Trim Batch 004 - Purple Punch`, - description: 'Process dried material from Batch 004', - status: 'PENDING', - priority: 'MEDIUM', - dueDate: new Date(Date.now() + 24 * 60 * 60 * 1000) // Tomorrow - }, - { - title: `${DEMO_PREFIX} Inventory Audit - PPE Supplies`, - description: 'Count and verify PPE stock levels, update system', - status: 'PENDING', - priority: 'LOW', - dueDate: new Date(Date.now() + 3 * 24 * 60 * 60 * 1000) // 3 days - }, - { - title: `${DEMO_PREFIX} Equipment Maintenance - HVAC Check`, - description: 'Quarterly HVAC filter replacement and system check', - status: 'COMPLETED', - priority: 'MEDIUM', - dueDate: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000) // 2 days ago - } - ]; - - for (const t of tasksData) { - const existing = await prisma.task.findFirst({ where: { title: t.title } }); - if (!existing) { - await prisma.task.create({ - data: { - ...t, - assignedToId: cultivator?.id, - batchId: createdBatches[0]?.id - } - }); - console.log(` โœ“ Created Task: ${t.title} (${t.status})`); - } - } - - // ==================== TREATMENT PROTOCOLS (NUTRIENTS) ==================== - console.log('\n๐Ÿงช Creating Treatment Protocols...'); - - // Front Row Ag 3-2-2 Mixing Task - const nutrientMixTask = { - title: `${DEMO_PREFIX} Mix Nutrient Test Stock (16 gal)`, - description: `Follow 16-gal Test Stock Protocol (Scale 0.32): -1. Part A Stock: Dissolve 24 lbs Part A into 16 gal RO. (Target EC ~2.9) -2. Part B Stock: Dissolve 16 lbs Part B + 864g PhosZyme into 16 gal RO. (Target EC ~1.69) -Procedure: Fill 8gal, add dry (5min), top up, mix 10min. -Validation: Test 250ml stock in 5gal RO.`, - status: 'PENDING', - priority: 'HIGH', - dueDate: new Date(Date.now() + 4 * 60 * 60 * 1000) // Due in 4 hours - }; - - const existingMix = await prisma.task.findFirst({ where: { title: nutrientMixTask.title } }); - if (!existingMix) { - await prisma.task.create({ + // ==================== ADMIN USER (Required) ==================== + console.log('\n๐Ÿ‘ค Creating Admin User...'); + const adminExists = await prisma.user.findUnique({ where: { email: 'admin@runfoo.run' } }); + if (!adminExists) { + const passwordHash = await hashPassword('password123'); + await prisma.user.create({ data: { - ...nutrientMixTask, - assignedToId: cultivator?.id, - batchId: createdBatches.find(b => b.name.includes('Gorilla Glue'))?.id + email: 'admin@runfoo.run', + passwordHash, + name: 'Travis (Owner)', + role: 'OWNER', + roleId: ownerRole?.id, + rate: 100.00 } }); - console.log(` โœ“ Created Task: ${nutrientMixTask.title}`); + console.log(' โœ“ Created Admin: admin@runfoo.run / password123'); } - // ==================== ANNOUNCEMENTS ==================== - console.log('\n๐Ÿ“ข Creating Demo Announcements...'); - const announcementsData = [ - { - title: `${DEMO_PREFIX} Welcome to 777 Wolfpack Grow Ops`, - body: 'This is a demo environment. All data shown is sample data for testing purposes. Login credentials: admin@runfoo.run / password123', - priority: 'INFO', - requiresAck: false, - createdById: manager?.id - }, - { - title: `${DEMO_PREFIX} Scheduled Facility Inspection - Dec 15`, - body: 'State inspector visit scheduled for December 15th. Please ensure all compliance documentation is up to date.', - priority: 'WARNING', - requiresAck: true, - createdById: manager?.id - } - ]; - - for (const a of announcementsData) { - if (a.createdById) { - const existing = await prisma.announcement.findFirst({ where: { title: a.title } }); - if (!existing) { - await prisma.announcement.create({ data: a }); - console.log(` โœ“ Created Announcement: ${a.title}`); - } - } - } - - console.log('\nโœจ Seeding complete!\n'); - console.log('Demo Login Credentials:'); - console.log(' Owner: admin@runfoo.run / password123'); - console.log(' Manager: manager@demo.local / demo1234'); - console.log(' Cultivator: cultivator@demo.local / demo1234'); - console.log(' Worker: worker@demo.local / demo1234'); + console.log('\nโœจ Base seeding complete!\n'); + console.log('Login: admin@runfoo.run / password123'); + console.log('\nFor demo data, run: npm run seed:demo'); } main()