ca-grow-ops-manager/backend/prisma/seed-demo.js
fullsizemalt 5c7a4b83c3
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
feat: comprehensive demo seed + paperless integration spec
Demo Seed (npm run seed:demo):
- 5 demo staff members
- 8 grow rooms
- 7 batches across all stages
- 292+ touch points with activity history
- 30 days of walkthrough history (with reservoir/irrigation/health checks)
- 9 SOPs and documents
- 12 supply items
- 7 tasks
- IPM schedules for active batches
- Weight logs
- 3 announcements
- 14 days time punch history

Paperless Integration Spec:
- API integration design for document archival
- Sync workflow (manual + automatic)
- Tagging conventions
- Document types
- Implementation phases
2025-12-12 20:37:28 -08:00

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