ca-grow-ops-manager/backend/prisma/seed-demo.js
fullsizemalt f02f0dfe72
Some checks are pending
Deploy to Production / deploy (push) Waiting to run
Test / backend-test (push) Waiting to run
Test / frontend-test (push) Waiting to run
fix: correct walkthrough field names in demo seed
2025-12-12 20:15:26 -08:00

393 lines
20 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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...');
// 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 = [
{ 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();
});