generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } enum RoleEnum { OWNER MANAGER GROWER STAFF } enum RoomType { VEG FLOWER DRY CURE MOTHER CLONE FACILITY } enum TaskStatus { PENDING IN_PROGRESS COMPLETED BLOCKED } // Daily Walkthrough Enums enum WalkthroughStatus { IN_PROGRESS COMPLETED INCOMPLETE } enum TankType { VEG FLOWER } enum TankStatus { OK LOW CRITICAL } enum HealthStatus { GOOD FAIR NEEDS_ATTENTION } enum AccessStatus { OK ISSUES } model User { id String @id @default(uuid()) email String @unique passwordHash String name String? role RoleEnum @default(STAFF) // Kept for legacy/fallback, but relying on roleId usually roleId String? userRole Role? @relation(fields: [roleId], references: [id]) rate Decimal? @map("hourly_rate") createdAt DateTime @default(now()) updatedAt DateTime @updatedAt tasks Task[] timeLogs TimeLog[] walkthroughs DailyWalkthrough[] touchPoints PlantTouchPoint[] // Phase 3 Relations weightLogs WeightLog[] batchNotes BatchNote[] batchPhotos BatchPhoto[] // Phase 8: Visitor Management visitorEscorts VisitorLog[] @relation("VisitorEscort") visitorApprovals VisitorLog[] @relation("VisitorApprover") // Phase 9: Messaging announcements Announcement[] @relation("AnnouncementCreator") announcementAcks AnnouncementAck[] @relation("AnnouncementAcker") shiftNotes ShiftNote[] @relation("ShiftNoteCreator") // Phase 10: Compliance documentsCreated Document[] @relation("DocumentCreator") documentsApproved Document[] @relation("DocumentApprover") @@map("users") } model Role { id String @id @default(uuid()) name String @unique description String? permissions Json // Store permissions as JSON: { users: { read: true, write: true }, ... } isSystem Boolean @default(false) // System roles cannot be deleted users User[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@map("roles") } model Room { id String @id @default(uuid()) name String type RoomType sqft Float? width Float? length Float? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt capacity Int? // plant count or sq ft status RoomStatus @default(ACTIVE) targetTemp Float? targetHumidity Float? targetVPD Float? batches Batch[] tasks Task[] @@map("rooms") } model Batch { id String @id @default(uuid()) name String // e.g., "B-2023-10-15-GG4" strain String startDate DateTime harvestDate DateTime? status String @default("ACTIVE") // ACTIVE, HARVESTED, COMPLETED plantCount Int @default(0) source BatchSource @default(CLONE) stage BatchStage @default(CLONE_IN) metrcTags String[] // array of METRC tag IDs roomId String? room Room? @relation(fields: [roomId], references: [id]) tasks Task[] touchPoints PlantTouchPoint[] ipmSchedule IPMSchedule? // Phase 3 Relations weights WeightLog[] notes BatchNote[] photos BatchPhoto[] facilityPlants FacilityPlant[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@map("batches") } model TaskTemplate { id String @id @default(uuid()) title String description String? // Instructions/SOP roomType RoomType? estimatedMinutes Int? materials String[] // Array of material names recurrence Json? // Cron or custom pattern createdAt DateTime @default(now()) updatedAt DateTime @updatedAt tasks Task[] @@map("task_templates") } model Task { id String @id @default(uuid()) title String description String? status TaskStatus @default(PENDING) priority String @default("MEDIUM") templateId String? template TaskTemplate? @relation(fields: [templateId], references: [id]) assignedToId String? assignedTo User? @relation(fields: [assignedToId], references: [id]) batchId String? batch Batch? @relation(fields: [batchId], references: [id]) roomId String? room Room? @relation(fields: [roomId], references: [id]) dueDate DateTime? completedAt DateTime? notes String? photos String[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@map("tasks") } model TimeLog { id String @id @default(uuid()) userId String user User @relation(fields: [userId], references: [id]) startTime DateTime endTime DateTime? activityType String? // e.g. "Trimming", "Feeding", "Cleaning" notes String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@map("time_logs") } // Daily Walkthrough Models model DailyWalkthrough { id String @id @default(uuid()) date DateTime @default(now()) completedBy String user User @relation(fields: [completedBy], references: [id]) startTime DateTime @default(now()) endTime DateTime? status WalkthroughStatus @default(IN_PROGRESS) reservoirChecks ReservoirCheck[] irrigationChecks IrrigationCheck[] plantHealthChecks PlantHealthCheck[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@map("daily_walkthroughs") } model WalkthroughSettings { id String @id @default("default") // Photo Requirements reservoirPhotos PhotoRequirement @default(OPTIONAL) irrigationPhotos PhotoRequirement @default(OPTIONAL) plantHealthPhotos PhotoRequirement @default(REQUIRED) // Enabled Sections enableReservoirs Boolean @default(true) enableIrrigation Boolean @default(true) enablePlantHealth Boolean @default(true) updatedAt DateTime @updatedAt @@map("walkthrough_settings") } enum PhotoRequirement { REQUIRED OPTIONAL WEEKLY ON_DEMAND } model ReservoirCheck { id String @id @default(uuid()) walkthroughId String walkthrough DailyWalkthrough @relation(fields: [walkthroughId], references: [id], onDelete: Cascade) tankName String tankType TankType levelPercent Int status TankStatus photoUrl String? notes String? createdAt DateTime @default(now()) @@map("reservoir_checks") } model IrrigationCheck { id String @id @default(uuid()) walkthroughId String walkthrough DailyWalkthrough @relation(fields: [walkthroughId], references: [id], onDelete: Cascade) zoneName String // "Veg Upstairs", "Veg Downstairs", "Flower Upstairs", "Flower Downstairs" drippersTotal Int drippersWorking Int drippersFailed String? // JSON array of failed dripper IDs waterFlow Boolean nutrientsMixed Boolean scheduleActive Boolean photoUrl String? // Photo of working system issues String? createdAt DateTime @default(now()) @@map("irrigation_checks") } model PlantHealthCheck { id String @id @default(uuid()) walkthroughId String walkthrough DailyWalkthrough @relation(fields: [walkthroughId], references: [id], onDelete: Cascade) zoneName String healthStatus HealthStatus pestsObserved Boolean pestType String? waterAccess AccessStatus foodAccess AccessStatus flaggedForAttention Boolean @default(false) issuePhotoUrl String? referencePhotoUrl String? // Photo of healthy system notes String? createdAt DateTime @default(now()) @@map("plant_health_checks") } // Supply/Shopping List models model SupplyItem { id String @id @default(uuid()) name String category SupplyCategory quantity Int @default(0) minThreshold Int @default(0) unit String // "each", "box", "roll", "gallon", etc. location String? // "Storage Room", "Bathroom", etc. vendor String? // "Amazon", "Local", etc. productUrl String? // Link to reorder lastOrdered DateTime? notes String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@map("supply_items") } enum SupplyCategory { FILTER // Air filters, water filters CLEANING // Cleaning supplies PPE // Gloves, masks, suits OFFICE // Paper, pens, etc. BATHROOM // Toilet paper, soap, etc. KITCHEN // Coffee, snacks, etc. MAINTENANCE // Tools, parts, etc. OTHER } // Plant Touch Points model PlantTouchPoint { id String @id @default(uuid()) type TouchType notes String? photoUrls String[] // Changed from single photoUrl to array // Measurements heightCm Float? widthCm Float? // IPM specific ipmProduct String? // e.g., "Pyganic 5.0" ipmDosage String? // e.g., "1 oz per gallon" // Issues issuesObserved Boolean @default(false) issueType String? batchId String batch Batch @relation(fields: [batchId], references: [id]) createdBy String user User @relation(fields: [createdBy], references: [id]) createdAt DateTime @default(now()) @@map("plant_touch_points") } enum TouchType { WATER FEED PRUNE TRAIN INSPECT IPM TRANSPLANT HARVEST OTHER } model IPMSchedule { id String @id @default(uuid()) batchId String @unique // One schedule per batch batch Batch @relation(fields: [batchId], references: [id]) product String // "Pyganic 5.0" intervalDays Int // 10 lastTreatment DateTime? nextTreatment DateTime? // Calculated isActive Boolean @default(true) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@map("ipm_schedules") } // --- Phase 3: Batch Lifecycle & Room Management --- enum BatchSource { CLONE SEED } enum BatchStage { CLONE_IN VEGETATIVE FLOWERING HARVEST DRYING CURING FINISHED } enum RoomStatus { ACTIVE CLEANING MAINTENANCE } enum WeightType { WET BUCKED FINAL_DRY TRIM WASTE } model WeightLog { id String @id @default(uuid()) batchId String batch Batch @relation(fields: [batchId], references: [id]) weightType WeightType weight Float // grams unit String @default("g") notes String? photos String[] // URLs loggedBy String user User @relation(fields: [loggedBy], references: [id]) createdAt DateTime @default(now()) @@map("weight_logs") } model BatchNote { id String @id @default(uuid()) batchId String batch Batch @relation(fields: [batchId], references: [id]) content String // markdown authorId String author User @relation(fields: [authorId], references: [id]) createdAt DateTime @default(now()) @@map("batch_notes") } model BatchPhoto { id String @id @default(uuid()) batchId String batch Batch @relation(fields: [batchId], references: [id]) url String caption String? uploadedBy String user User @relation(fields: [uploadedBy], references: [id]) createdAt DateTime @default(now()) @@map("batch_photos") } // ============================================ // FACILITY LAYOUT MODELS (Phase 7) // ============================================ enum SectionType { TABLE RACK TRAY HANGER FLOOR } // Plant Type Library (Rackula-inspired DeviceType pattern) enum PlantCategory { VEG FLOWER MOTHER CLONE SEEDLING } model PlantType { id String @id @default(uuid()) slug String @unique // kebab-case identifier (e.g., "gorilla-glue-4") name String // Display name strain String? // Strain name category PlantCategory // VEG, FLOWER, MOTHER, CLONE, SEEDLING colour String // Hex color for display (e.g., "#4A90D9") growthDays Int? // Expected days to harvest yieldGrams Float? // Expected yield in grams notes String? tags String[] customFields Json? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@map("plant_types") } model FacilityProperty { id String @id @default(uuid()) name String address String? licenseNum String? // DCC License Number buildings FacilityBuilding[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@map("facility_properties") } model FacilityBuilding { id String @id @default(uuid()) property FacilityProperty @relation(fields: [propertyId], references: [id], onDelete: Cascade) propertyId String name String code String // Short code: "GROW1" type String // CULTIVATION, PROCESSING, DRYING, STORAGE floors FacilityFloor[] layout Json? // Visual layout data createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@map("facility_buildings") } model FacilityFloor { id String @id @default(uuid()) building FacilityBuilding @relation(fields: [buildingId], references: [id], onDelete: Cascade) buildingId String name String // "Floor 1", "Ground Floor" number Int // 1, 2, 3... width Int // feet height Int // feet ceilingHeight Float? // feet defaultTiers Int @default(1) rooms FacilityRoom[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@map("facility_floors") } model FacilityRoom { id String @id @default(uuid()) floor FacilityFloor @relation(fields: [floorId], references: [id], onDelete: Cascade) floorId String name String code String // "VEG-A" type RoomType // VEG, FLOWER, etc. posX Int // Position X in pixels posY Int // Position Y in pixels width Int // Width in pixels height Int // Height in pixels rotation Int @default(0) color String? // Custom color override sections FacilitySection[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@map("facility_rooms") } model FacilitySection { id String @id @default(uuid()) room FacilityRoom @relation(fields: [roomId], references: [id], onDelete: Cascade) roomId String name String // "Table 1" code String // "T1" type SectionType posX Int // Relative to room posY Int width Int height Int rows Int // Grid rows columns Int // Grid columns tiers Int @default(1) // Number of vertical tiers spacing Int @default(12) // inches between positions positions FacilityPosition[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@map("facility_sections") } model FacilityPosition { id String @id @default(uuid()) section FacilitySection @relation(fields: [sectionId], references: [id], onDelete: Cascade) sectionId String row Int column Int tier Int @default(1) // Vertical level (1 = bottom) slot Int @default(1) // For multi-plant positions status String @default("EMPTY") // EMPTY, OCCUPIED, RESERVED, DAMAGED plant FacilityPlant? @@unique([sectionId, row, column, tier, slot]) @@map("facility_positions") } model FacilityPlant { id String @id @default(uuid()) tagNumber String @unique // METRC tag batchId String? batch Batch? @relation(fields: [batchId], references: [id]) plantTypeId String? plantType PlantType? @relation(fields: [plantTypeId], references: [id]) position FacilityPosition @relation(fields: [positionId], references: [id]) positionId String @unique address String // Full hierarchical address status String @default("ACTIVE") // ACTIVE, HARVESTED, DESTROYED, TRANSFERRED history PlantLocationHistory[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@map("facility_plants") } model PlantLocationHistory { id String @id @default(uuid()) plant FacilityPlant @relation(fields: [plantId], references: [id], onDelete: Cascade) plantId String fromAddress String? toAddress String movedById String // User ID reason String? // TRANSPLANT, REORGANIZE, HARVEST movedAt DateTime @default(now()) @@map("plant_location_history") } // ============================================================================= // PHASE 8: VISITOR MANAGEMENT // ============================================================================= enum VisitorType { VISITOR CONTRACTOR INSPECTOR VENDOR DELIVERY OTHER } enum VisitorStatus { PRE_REGISTERED CHECKED_IN CHECKED_OUT DENIED REVOKED } model Visitor { id String @id @default(uuid()) name String company String? email String? phone String? photoUrl String? idType String? // Driver License, Passport, etc. idNumber String? type VisitorType @default(VISITOR) purpose String ndaAccepted Boolean @default(false) ndaAcceptedAt DateTime? notes String? logs VisitorLog[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@map("visitors") } model VisitorLog { id String @id @default(uuid()) visitor Visitor @relation(fields: [visitorId], references: [id], onDelete: Cascade) visitorId String status VisitorStatus @default(CHECKED_IN) entryTime DateTime @default(now()) exitTime DateTime? escort User? @relation("VisitorEscort", fields: [escortId], references: [id]) escortId String? approvedBy User? @relation("VisitorApprover", fields: [approvedById], references: [id]) approvedById String? badgeNumber String? badgeExpiry DateTime? zones String[] // AccessZone IDs visited signature String? // Base64 or URL temperatureF Float? // Health screening notes String? @@map("visitor_logs") } model AccessZone { id String @id @default(uuid()) name String code String @unique description String? escortRequired Boolean @default(false) badgeRequired Boolean @default(true) ndaRequired Boolean @default(false) allowedTypes VisitorType[] maxOccupancy Int? parentZoneId String? parentZone AccessZone? @relation("ZoneHierarchy", fields: [parentZoneId], references: [id]) childZones AccessZone[] @relation("ZoneHierarchy") createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@map("access_zones") } // ============================================================================= // PHASE 9: MESSAGING & ANNOUNCEMENTS // ============================================================================= enum AnnouncementPriority { INFO WARNING CRITICAL } model Announcement { id String @id @default(uuid()) title String body String priority AnnouncementPriority @default(INFO) requiresAck Boolean @default(false) targetRoles String[] // Which roles see this expiresAt DateTime? createdBy User @relation("AnnouncementCreator", fields: [createdById], references: [id]) createdById String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt acks AnnouncementAck[] @@map("announcements") } model AnnouncementAck { id String @id @default(uuid()) announcement Announcement @relation(fields: [announcementId], references: [id], onDelete: Cascade) announcementId String user User @relation("AnnouncementAcker", fields: [userId], references: [id]) userId String readAt DateTime? acknowledgedAt DateTime? @@unique([announcementId, userId]) @@map("announcement_acks") } // Shift notes for handoff communication model ShiftNote { id String @id @default(uuid()) roomId String? batchId String? content String importance String @default("NORMAL") // LOW, NORMAL, HIGH, URGENT readBy String[] // User IDs who have read this createdBy User @relation("ShiftNoteCreator", fields: [createdById], references: [id]) createdById String createdAt DateTime @default(now()) expiresAt DateTime? // Auto-expire old notes @@map("shift_notes") } // ============================================================================= // PHASE 10: COMPLIANCE & AUDIT TRAIL // ============================================================================= enum AuditAction { CREATE UPDATE DELETE LOGIN LOGOUT ACCESS EXPORT APPROVE REJECT } // Immutable audit log - append only model AuditLog { id String @id @default(uuid()) userId String? userName String? // Denormalized for persistence action AuditAction entity String // Model name: Batch, Task, Plant, etc. entityId String? entityName String? // Denormalized description before Json? // State before change after Json? // State after change changes Json? // Only the changed fields ipAddress String? userAgent String? sessionId String? metadata Json? // Additional context timestamp DateTime @default(now()) @@index([userId]) @@index([entity, entityId]) @@index([timestamp]) @@map("audit_logs") } enum DocumentType { SOP POLICY TRAINING FORM CHECKLIST OTHER } enum DocumentStatus { DRAFT PENDING_REVIEW APPROVED ARCHIVED } model Document { id String @id @default(uuid()) title String type DocumentType status DocumentStatus @default(DRAFT) content String // Markdown or HTML version Int @default(1) effectiveDate DateTime? expiresAt DateTime? requiresAck Boolean @default(false) targetRoles String[] // Who must read this createdBy User @relation("DocumentCreator", fields: [createdById], references: [id]) createdById String approvedBy User? @relation("DocumentApprover", fields: [approvedById], references: [id]) approvedById String? approvedAt DateTime? versions DocumentVersion[] acks DocumentAck[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@map("documents") } model DocumentVersion { id String @id @default(uuid()) document Document @relation(fields: [documentId], references: [id], onDelete: Cascade) documentId String version Int title String content String changedBy String // User ID changeNotes String? createdAt DateTime @default(now()) @@unique([documentId, version]) @@map("document_versions") } model DocumentAck { id String @id @default(uuid()) document Document @relation(fields: [documentId], references: [id], onDelete: Cascade) documentId String userId String version Int // Which version was acknowledged acknowledgedAt DateTime @default(now()) @@unique([documentId, userId]) @@map("document_acks") } // ============================================================================= // PHASE 13: ADVANCED FEATURES // ============================================================================= // ---------------------- Environmental Monitoring ---------------------- enum SensorType { TEMPERATURE HUMIDITY CO2 LIGHT_PAR LIGHT_LUX PH EC VPD SOIL_MOISTURE AIR_FLOW } model Sensor { id String @id @default(uuid()) name String type SensorType roomId String? location String? // e.g., "North wall", "Canopy level" deviceId String? // Hardware device ID manufacturer String? model String? isActive Boolean @default(true) minThreshold Float? maxThreshold Float? readings SensorReading[] alerts EnvironmentAlert[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([roomId]) @@index([type]) @@map("sensors") } model SensorReading { id String @id @default(uuid()) sensor Sensor @relation(fields: [sensorId], references: [id], onDelete: Cascade) sensorId String value Float unit String // °F, %, ppm, μmol/m²/s, etc. timestamp DateTime @default(now()) @@index([sensorId, timestamp]) @@map("sensor_readings") } enum AlertSeverity { INFO WARNING CRITICAL EMERGENCY } model EnvironmentAlert { id String @id @default(uuid()) sensor Sensor? @relation(fields: [sensorId], references: [id]) sensorId String? roomId String? type String // TEMP_HIGH, HUMIDITY_LOW, CO2_HIGH, etc. severity AlertSeverity message String value Float? threshold Float? acknowledgedAt DateTime? acknowledgedBy String? resolvedAt DateTime? resolvedBy String? createdAt DateTime @default(now()) @@index([roomId, createdAt]) @@index([severity]) @@map("environment_alerts") } // Environment targets per room/stage model EnvironmentProfile { id String @id @default(uuid()) name String stage String? // CLONE, VEG, FLOWER, DRY, CURE roomId String? tempMinF Float? tempMaxF Float? humidityMin Float? humidityMax Float? co2Min Float? co2Max Float? lightHours Float? vpdMin Float? vpdMax Float? isDefault Boolean @default(false) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@map("environment_profiles") } // ---------------------- Financial Tracking ---------------------- enum TransactionType { EXPENSE REVENUE ADJUSTMENT } enum ExpenseCategory { LABOR NUTRIENTS SUPPLIES EQUIPMENT UTILITIES RENT LICENSES TESTING PACKAGING MARKETING OTHER } model FinancialTransaction { id String @id @default(uuid()) type TransactionType category ExpenseCategory? amount Float description String batchId String? // Link to batch for cost tracking roomId String? vendorId String? invoiceNumber String? date DateTime @default(now()) createdBy String attachments String[] // URLs to receipts/invoices tags String[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([type, date]) @@index([batchId]) @@index([category]) @@map("financial_transactions") } // Cost aggregation per batch model BatchCost { id String @id @default(uuid()) batchId String @unique laborCost Float @default(0) nutrientCost Float @default(0) utilityCost Float @default(0) supplyCost Float @default(0) otherCost Float @default(0) totalCost Float @default(0) plantCount Int @default(0) costPerPlant Float? yieldGrams Float? costPerGram Float? updatedAt DateTime @updatedAt @@map("batch_costs") } // Revenue tracking model Sale { id String @id @default(uuid()) batchId String? product String quantity Float unit String // grams, ounces, units pricePerUnit Float totalPrice Float customerId String? invoiceId String? date DateTime @default(now()) notes String? createdBy String createdAt DateTime @default(now()) @@index([batchId]) @@index([date]) @@map("sales") } // ---------------------- AI/ML Predictions ---------------------- model YieldPrediction { id String @id @default(uuid()) batchId String predictedYield Float // grams confidence Float // 0-1 factors Json? // Contributing factors predictedAt DateTime @default(now()) actualYield Float? // Filled in after harvest accuracy Float? // Calculated after actual @@index([batchId]) @@map("yield_predictions") } model AnomalyDetection { id String @id @default(uuid()) entityType String // Batch, Room, Sensor entityId String anomalyType String // GROWTH_SLOW, YIELD_LOW, ENV_UNSTABLE severity String description String data Json? isResolved Boolean @default(false) detectedAt DateTime @default(now()) @@index([entityType, entityId]) @@map("anomaly_detections") }