- Refactored navigation with grouped sections (Operations, Cultivation, Analytics, etc.) - Added RBAC-based navigation filtering by user role - Created DevTools panel for quick user switching during testing - Added collapsible sidebar sections on desktop - Mobile: bottom nav bar (4 items + More) with slide-up sheet - Enhanced seed data with [DEMO] prefix markers - Added multiple demo users: Owner, Manager, Cultivator, Worker - Fixed domain to runfoo.run - Added Audit Log and SOP Library pages to navigation - Created usePermissions hook and RoleBadge component
373 lines
15 KiB
JavaScript
373 lines
15 KiB
JavaScript
const { PrismaClient, RoomType } = require('@prisma/client');
|
||
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');
|
||
|
||
// ==================== ROLES ====================
|
||
console.log('📋 Creating Roles...');
|
||
const rolesData = [
|
||
{
|
||
name: 'Facility Owner',
|
||
description: 'Full access to all features and settings',
|
||
permissions: { admin: true, all: true },
|
||
isSystem: true
|
||
},
|
||
{
|
||
name: 'Manager',
|
||
description: 'Operational control and reporting',
|
||
permissions: {
|
||
users: { view: true, manage: true },
|
||
tasks: { view: true, manage: true, assign: true },
|
||
inventory: { view: true, manage: true },
|
||
reports: { view: true, export: true },
|
||
visitors: { view: true, manage: true },
|
||
compliance: { view: true }
|
||
},
|
||
isSystem: true
|
||
},
|
||
{
|
||
name: 'Cultivator',
|
||
description: 'Plant care and daily operations',
|
||
permissions: {
|
||
tasks: { view: true, complete: true },
|
||
inventory: { view: true },
|
||
batches: { view: true, update: true },
|
||
rooms: { view: true }
|
||
},
|
||
isSystem: true
|
||
},
|
||
{
|
||
name: 'Worker',
|
||
description: 'Basic task completion and logging',
|
||
permissions: {
|
||
tasks: { view: true, complete: true },
|
||
timeclock: { view: true, punch: true }
|
||
},
|
||
isSystem: true
|
||
},
|
||
{
|
||
name: 'Viewer',
|
||
description: 'Read-only access to dashboards',
|
||
permissions: {
|
||
dashboard: { view: true },
|
||
reports: { view: true }
|
||
},
|
||
isSystem: true
|
||
}
|
||
];
|
||
|
||
for (const r of rolesData) {
|
||
const existing = await prisma.role.findUnique({ where: { name: r.name } });
|
||
if (!existing) {
|
||
await prisma.role.create({ data: r });
|
||
console.log(` ✓ Created Role: ${r.name}`);
|
||
}
|
||
}
|
||
|
||
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: 'Cultivator' } });
|
||
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: 'CULTIVATOR',
|
||
roleId: cultivatorRole?.id,
|
||
rate: 28.00
|
||
},
|
||
{
|
||
email: 'worker@demo.local',
|
||
password: 'demo1234',
|
||
name: `${DEMO_PREFIX} Alex Rivera`,
|
||
role: 'WORKER',
|
||
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 cultivator = await prisma.user.findFirst({ where: { email: 'cultivator@demo.local' } });
|
||
|
||
const batchesData = [
|
||
{
|
||
name: `${DEMO_PREFIX} OG Kush - Batch 001`,
|
||
strain: 'OG Kush',
|
||
status: 'ACTIVE',
|
||
stage: 'VEGETATIVE',
|
||
source: 'CLONE',
|
||
plantCount: 48,
|
||
startDate: new Date(Date.now() - 14 * 24 * 60 * 60 * 1000), // 14 days ago
|
||
roomId: createdRooms.find(r => r.name.includes('Veg Room 1'))?.id
|
||
},
|
||
{
|
||
name: `${DEMO_PREFIX} Blue Dream - Batch 002`,
|
||
strain: 'Blue Dream',
|
||
status: 'ACTIVE',
|
||
stage: 'FLOWERING',
|
||
source: 'CLONE',
|
||
plantCount: 36,
|
||
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} Girl Scout Cookies - Batch 003`,
|
||
strain: 'Girl Scout Cookies',
|
||
status: 'ACTIVE',
|
||
stage: 'FLOWERING',
|
||
source: 'SEED',
|
||
plantCount: 24,
|
||
startDate: new Date(Date.now() - 52 * 24 * 60 * 60 * 1000), // 52 days ago
|
||
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: 'NUTRIENTS', quantity: 450, minThreshold: 200, unit: 'cube', location: 'Veg Storage', vendor: 'GrowGen' },
|
||
{ name: `${DEMO_PREFIX} Coco Coir`, category: 'NUTRIENTS', 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' },
|
||
];
|
||
|
||
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 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,
|
||
createdById: manager?.id,
|
||
assignedToId: cultivator?.id,
|
||
batchId: createdBatches[0]?.id
|
||
}
|
||
});
|
||
console.log(` ✓ Created Task: ${t.title} (${t.status})`);
|
||
}
|
||
}
|
||
|
||
// ==================== 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');
|
||
}
|
||
|
||
main()
|
||
.catch((e) => {
|
||
console.error('Seeding failed:', e);
|
||
process.exit(1);
|
||
})
|
||
.finally(async () => {
|
||
await prisma.$disconnect();
|
||
});
|