396 lines
20 KiB
JavaScript
396 lines
20 KiB
JavaScript
/**
|
||
* 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 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();
|
||
});
|