/** * 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; } // ==================== FACILITY STRUCTURE ==================== console.log('\nšŸ—ļø Creating Facility Structure...'); // Create Property let property = await prisma.facilityProperty.findFirst({ where: { name: `${DEMO_PREFIX} 777 Wolfpack Cultivation` } }); if (!property) { property = await prisma.facilityProperty.create({ data: { name: `${DEMO_PREFIX} 777 Wolfpack Cultivation`, address: '1234 Grow Lane, Grass Valley, CA 95945', licenseNum: 'CCL21-0001234' } }); console.log(' āœ“ Property: 777 Wolfpack Cultivation'); } // Create Buildings const buildingsData = [ { name: 'Main Cultivation', code: 'GROW1', type: 'CULTIVATION' }, { name: 'Processing Facility', code: 'PROC1', type: 'PROCESSING' }, ]; const buildings = {}; for (const b of buildingsData) { let building = await prisma.facilityBuilding.findFirst({ where: { code: b.code, propertyId: property.id } }); if (!building) { building = await prisma.facilityBuilding.create({ data: { ...b, propertyId: property.id } }); console.log(` āœ“ Building: ${b.name}`); } buildings[b.code] = building; } // Create Floors const floorsData = [ { buildingCode: 'GROW1', name: 'Ground Floor', number: 1, width: 100, height: 80, ceilingHeight: 12 }, { buildingCode: 'GROW1', name: 'Upper Floor', number: 2, width: 100, height: 80, ceilingHeight: 10 }, { buildingCode: 'PROC1', name: 'Ground Floor', number: 1, width: 60, height: 40, ceilingHeight: 10 }, ]; const floors = {}; for (const f of floorsData) { const buildingId = buildings[f.buildingCode]?.id; if (!buildingId) continue; let floor = await prisma.facilityFloor.findFirst({ where: { number: f.number, buildingId } }); if (!floor) { floor = await prisma.facilityFloor.create({ data: { buildingId, name: f.name, number: f.number, width: f.width, height: f.height, ceilingHeight: f.ceilingHeight } }); console.log(` āœ“ Floor: ${f.buildingCode} - ${f.name}`); } floors[`${f.buildingCode}-F${f.number}`] = floor; } // Create Facility Rooms with Sections (mapped to grow rooms) const facilityRoomsData = [ { floorKey: 'GROW1-F1', name: 'Veg Room 1', code: 'VEG-A', type: 'VEG', posX: 10, posY: 10, width: 800, height: 600 }, { floorKey: 'GROW1-F1', name: 'Veg Room 2', code: 'VEG-B', type: 'VEG', posX: 850, posY: 10, width: 700, height: 500 }, { floorKey: 'GROW1-F1', name: 'Mother Room', code: 'MTHR', type: 'MOTHER', posX: 10, posY: 650, width: 400, height: 400 }, { floorKey: 'GROW1-F2', name: 'Flower Room A', code: 'FLR-A', type: 'FLOWER', posX: 50, posY: 50, width: 900, height: 700 }, { floorKey: 'GROW1-F2', name: 'Flower Room B', code: 'FLR-B', type: 'FLOWER', posX: 1000, posY: 50, width: 900, height: 700 }, { floorKey: 'GROW1-F2', name: 'Flower Room C', code: 'FLR-C', type: 'FLOWER', posX: 50, posY: 800, width: 800, height: 600 }, { floorKey: 'PROC1-F1', name: 'Dry Room', code: 'DRY', type: 'DRY', posX: 50, posY: 50, width: 500, height: 600 }, { floorKey: 'PROC1-F1', name: 'Cure Room', code: 'CURE', type: 'CURE', posX: 600, posY: 50, width: 400, height: 600 }, ]; const facilityRooms = {}; for (const fr of facilityRoomsData) { const floorId = floors[fr.floorKey]?.id; if (!floorId) continue; let room = await prisma.facilityRoom.findFirst({ where: { code: fr.code, floorId } }); if (!room) { room = await prisma.facilityRoom.create({ data: { floorId, name: fr.name, code: fr.code, type: fr.type, posX: fr.posX, posY: fr.posY, width: fr.width, height: fr.height } }); } facilityRooms[fr.code] = room; } console.log(' āœ“ 8 facility rooms mapped'); // Create Sections (Tables/Beds) with Plant Positions console.log('\n🌿 Creating Grow Sections...'); const sectionsData = [ // Veg Room 1 - 4 tables, 4x4 each { roomCode: 'VEG-A', name: 'Table 1', code: 'T1', type: 'TABLE', posX: 50, posY: 50, width: 160, height: 320, rows: 4, columns: 4 }, { roomCode: 'VEG-A', name: 'Table 2', code: 'T2', type: 'TABLE', posX: 230, posY: 50, width: 160, height: 320, rows: 4, columns: 4 }, { roomCode: 'VEG-A', name: 'Table 3', code: 'T3', type: 'TABLE', posX: 410, posY: 50, width: 160, height: 320, rows: 4, columns: 4 }, { roomCode: 'VEG-A', name: 'Table 4', code: 'T4', type: 'TABLE', posX: 590, posY: 50, width: 160, height: 320, rows: 4, columns: 4 }, // Flower Room A - 6 tables, 5x8 each (for flowering plants) { roomCode: 'FLR-A', name: 'Table A1', code: 'A1', type: 'TABLE', posX: 50, posY: 50, width: 200, height: 280, rows: 5, columns: 8 }, { roomCode: 'FLR-A', name: 'Table A2', code: 'A2', type: 'TABLE', posX: 270, posY: 50, width: 200, height: 280, rows: 5, columns: 8 }, { roomCode: 'FLR-A', name: 'Table A3', code: 'A3', type: 'TABLE', posX: 490, posY: 50, width: 200, height: 280, rows: 5, columns: 8 }, { roomCode: 'FLR-A', name: 'Table A4', code: 'A4', type: 'TABLE', posX: 50, posY: 370, width: 200, height: 280, rows: 5, columns: 8 }, { roomCode: 'FLR-A', name: 'Table A5', code: 'A5', type: 'TABLE', posX: 270, posY: 370, width: 200, height: 280, rows: 5, columns: 8 }, { roomCode: 'FLR-A', name: 'Table A6', code: 'A6', type: 'TABLE', posX: 490, posY: 370, width: 200, height: 280, rows: 5, columns: 8 }, // Flower Room B - 6 tables { roomCode: 'FLR-B', name: 'Table B1', code: 'B1', type: 'TABLE', posX: 50, posY: 50, width: 200, height: 280, rows: 5, columns: 8 }, { roomCode: 'FLR-B', name: 'Table B2', code: 'B2', type: 'TABLE', posX: 270, posY: 50, width: 200, height: 280, rows: 5, columns: 8 }, { roomCode: 'FLR-B', name: 'Table B3', code: 'B3', type: 'TABLE', posX: 490, posY: 50, width: 200, height: 280, rows: 5, columns: 8 }, { roomCode: 'FLR-B', name: 'Table B4', code: 'B4', type: 'TABLE', posX: 50, posY: 370, width: 200, height: 280, rows: 5, columns: 8 }, { roomCode: 'FLR-B', name: 'Table B5', code: 'B5', type: 'TABLE', posX: 270, posY: 370, width: 200, height: 280, rows: 5, columns: 8 }, { roomCode: 'FLR-B', name: 'Table B6', code: 'B6', type: 'TABLE', posX: 490, posY: 370, width: 200, height: 280, rows: 5, columns: 8 }, // Mother Room - 2 tables { roomCode: 'MTHR', name: 'Mother Table 1', code: 'M1', type: 'TABLE', posX: 50, posY: 50, width: 140, height: 280, rows: 4, columns: 3 }, { roomCode: 'MTHR', name: 'Mother Table 2', code: 'M2', type: 'TABLE', posX: 220, posY: 50, width: 140, height: 280, rows: 4, columns: 3 }, // Dry Room - Drying racks { roomCode: 'DRY', name: 'Dry Rack 1', code: 'D1', type: 'RACK', posX: 50, posY: 50, width: 180, height: 240, rows: 6, columns: 4 }, { roomCode: 'DRY', name: 'Dry Rack 2', code: 'D2', type: 'RACK', posX: 260, posY: 50, width: 180, height: 240, rows: 6, columns: 4 }, // Cure Room - Cure shelves { roomCode: 'CURE', name: 'Cure Shelf 1', code: 'C1', type: 'RACK', posX: 50, posY: 50, width: 140, height: 480, rows: 8, columns: 3 }, { roomCode: 'CURE', name: 'Cure Shelf 2', code: 'C2', type: 'RACK', posX: 220, posY: 50, width: 140, height: 480, rows: 8, columns: 3 }, ]; let sectionCount = 0; let positionCount = 0; for (const s of sectionsData) { const room = facilityRooms[s.roomCode]; if (!room) continue; let section = await prisma.facilitySection.findFirst({ where: { code: s.code, roomId: room.id } }); if (!section) { section = await prisma.facilitySection.create({ data: { roomId: room.id, name: s.name, code: s.code, type: s.type, posX: s.posX, posY: s.posY, width: s.width, height: s.height, rows: s.rows, columns: s.columns, spacing: 12 } }); sectionCount++; // Create plant positions for this section for (let row = 0; row < s.rows; row++) { for (let col = 0; col < s.columns; col++) { await prisma.facilityPosition.create({ data: { sectionId: section.id, row: row + 1, column: col + 1, status: 'EMPTY' } }); positionCount++; } } } } console.log(` āœ“ ${sectionCount} sections with ${positionCount} plant positions`); // ==================== 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...'); // Must match TouchType enum: WATER, FEED, PRUNE, TRAIN, INSPECT, IPM, TRANSPLANT, HARVEST, OTHER const touchPointTypes = ['WATER', 'FEED', 'INSPECT', 'PRUNE', 'TRAIN', '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'], PRUNE: ['Removed lower fan leaves', 'Light lollipop', 'Heavy defoliation day 21'], TRAIN: ['Topped to 4 nodes', 'FIMed', 'LST adjustment', 'Supercrop main stem'], 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, createdBy: 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 startTime = daysAgo(d); const endTime = new Date(startTime); endTime.setMinutes(endTime.getMinutes() + 30 + Math.floor(Math.random() * 30)); const walkthrough = await prisma.dailyWalkthrough.create({ data: { completedBy: user.id, status: 'COMPLETED', date: daysAgo(d), startTime, endTime } }); // 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 = [ { title: `${DEMO_PREFIX} SOP - Daily Walkthrough Protocol`, type: 'SOP', content: '# Daily Walkthrough Protocol\n\n## Purpose\nEnsure all grow rooms are inspected daily.\n\n## Steps\n1. Check reservoir levels\n2. Verify irrigation systems\n3. Inspect plant health\n4. Log any issues' }, { title: `${DEMO_PREFIX} SOP - IPM Treatment Schedule`, type: 'SOP', content: '# IPM Treatment Schedule\n\n## Products\n- Pyganic 5.0\n- Dr Zymes\n- Regalia\n\n## Frequency\nEvery 7-14 days during veg and early flower.' }, { title: `${DEMO_PREFIX} SOP - Nutrient Mixing`, type: 'SOP', content: '# Nutrient Mixing (Front Row Ag)\n\n## Stock Preparation\n1. Part A: 24lbs per 16 gal\n2. Part B: 16lbs + PhosZyme per 16 gal' }, { title: `${DEMO_PREFIX} SOP - Clone Processing`, type: 'SOP', content: '# Clone Processing\n\n## Receiving\n1. Inspect for pests\n2. Count and verify\n3. Log in METRC' }, { title: `${DEMO_PREFIX} SOP - Harvest Procedure`, type: 'SOP', content: '# Harvest Procedure\n\n## Steps\n1. Cut main stem\n2. Hang dry in Dry Room\n3. Monitor temp/humidity\n4. Trim when stems snap' }, { title: `${DEMO_PREFIX} SOP - Visitor Check-In`, type: 'POLICY', content: '# Visitor Check-In\n\n## Requirements\n1. Valid ID\n2. Signed NDA\n3. PPE issued' }, { title: `${DEMO_PREFIX} SOP - Emergency Procedures`, type: 'POLICY', content: '# Emergency Procedures\n\n## Fire\n1. Evacuate\n2. Call 911\n3. Meet at designated point' }, { title: `${DEMO_PREFIX} HVAC Maintenance`, type: 'TRAINING', content: '# HVAC Maintenance\n\n## Quarterly Tasks\n- Replace filters\n- Check refrigerant\n- Clean coils' }, { title: `${DEMO_PREFIX} METRC Training Guide`, type: 'TRAINING', content: '# METRC Training\n\n## Logging Plants\n1. Scan package tag\n2. Enter plant count\n3. Verify location' }, ]; for (const doc of documents) { const existing = await prisma.document.findFirst({ where: { title: doc.title } }); if (!existing && users.sarah?.id) { await prisma.document.create({ data: { ...doc, status: 'APPROVED', version: 1, createdById: users.sarah.id } }); console.log(` āœ“ ${doc.title}`); } } // ==================== 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} HVAC Filters`, category: 'FILTER', quantity: 24, minThreshold: 12, unit: 'each', location: 'Utility Room', vendor: 'Amazon' }, { name: `${DEMO_PREFIX} Carbon Filters`, category: 'FILTER', quantity: 8, minThreshold: 4, unit: 'each', location: 'Utility Room', vendor: 'Phresh' }, { name: `${DEMO_PREFIX} Front Row Ag - Part A`, category: 'OTHER', quantity: 40, minThreshold: 10, unit: 'bag (25lb)', location: 'Nutrient Storage', vendor: 'Front Row Ag' }, { name: `${DEMO_PREFIX} Front Row Ag - Part B`, category: 'OTHER', 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 && users.mike?.id) { await prisma.weightLog.create({ data: { batchId: curingBatch.id, loggedBy: users.mike.id, weightType: 'FINAL_DRY', weight: 8450, notes: 'Final dry weight before cure' } }); } if (dryingBatch && users.jordan?.id) { await prisma.weightLog.create({ data: { batchId: dryingBatch.id, loggedBy: users.jordan.id, weightType: 'WET', weight: 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 startTime = new Date(daysAgo(d)); startTime.setHours(7 + Math.floor(Math.random() * 2), Math.floor(Math.random() * 30), 0); const endTime = new Date(startTime); endTime.setHours(startTime.getHours() + 8 + Math.floor(Math.random() * 2)); await prisma.timeLog.create({ data: { userId: user.id, startTime, endTime, notes: Math.random() > 0.8 ? 'Overtime for trim' : null } }); } } } console.log(' āœ“ 14 days of time punch history'); // ==================== AUDIT LOGS ==================== console.log('\nšŸ“‹ Creating Audit Log Entries...'); const auditEntries = [ { userId: users.admin?.id, userName: 'Admin User', action: 'LOGIN', entity: 'User', entityName: 'Admin login from office', ipAddress: '192.168.1.50' }, { userId: users.sarah?.id, userName: 'Sarah Chen', action: 'CREATE', entity: 'Batch', entityId: Object.values(batches)[0]?.id, entityName: 'Created batch: Gorilla Glue #4 - B001' }, { userId: users.mike?.id, userName: 'Mike Thompson', action: 'UPDATE', entity: 'Task', entityName: 'Updated task status to completed', changes: { status: { from: 'PENDING', to: 'COMPLETED' } } }, { userId: users.sarah?.id, userName: 'Sarah Chen', action: 'CREATE', entity: 'Document', entityName: 'Added SOP: Daily Walkthrough Protocol' }, { userId: users.admin?.id, userName: 'Admin User', action: 'UPDATE', entity: 'Room', entityName: 'Updated Flower Room A climate settings', changes: { targetTemp: { from: 72, to: 75 } } }, { userId: users.alex?.id, userName: 'Alex Rivera', action: 'ACCESS', entity: 'Report', entityName: 'Accessed Weekly Production Report' }, { userId: users.sarah?.id, userName: 'Sarah Chen', action: 'APPROVE', entity: 'TimeLog', entityName: 'Approved overtime for Mike Thompson' }, { userId: users.mike?.id, userName: 'Mike Thompson', action: 'UPDATE', entity: 'Batch', entityId: Object.values(batches)[1]?.id, entityName: 'Moved batch B002 to Vegetative stage', changes: { stage: { from: 'CLONE_IN', to: 'VEGETATIVE' } } }, { userId: users.jordan?.id, userName: 'Jordan Lee', action: 'CREATE', entity: 'TouchPoint', entityName: 'Logged watering for batch B003' }, { userId: users.admin?.id, userName: 'Admin User', action: 'EXPORT', entity: 'Report', entityName: 'Exported compliance report to PDF' }, { userId: users.sarah?.id, userName: 'Sarah Chen', action: 'CREATE', entity: 'Visitor', entityName: 'Registered visitor: State Inspector' }, { userId: users.mike?.id, userName: 'Mike Thompson', action: 'UPDATE', entity: 'Plant', entityName: 'Updated plant health status', changes: { healthStatus: { from: 'GOOD', to: 'NEEDS_ATTENTION' } } }, { userId: users.sam?.id, userName: 'Sam Martinez', action: 'CREATE', entity: 'SupplyItem', entityName: 'Added new supply: Pyganic 5.0' }, { userId: users.sarah?.id, userName: 'Sarah Chen', action: 'DELETE', entity: 'Announcement', entityName: 'Removed expired announcement' }, { userId: users.admin?.id, userName: 'Admin User', action: 'UPDATE', entity: 'Role', entityName: 'Updated permissions for Grower role' }, ]; for (let i = 0; i < auditEntries.length; i++) { const entry = auditEntries[i]; const timestamp = daysAgo(i % 14); timestamp.setHours(8 + Math.floor(Math.random() * 10), Math.floor(Math.random() * 60)); await prisma.auditLog.create({ data: { ...entry, changes: entry.changes ? JSON.stringify(entry.changes) : null, metadata: null, timestamp } }); } console.log(` āœ“ ${auditEntries.length} audit log entries`); 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(); });