feat(ui): Integrate AuraUI Hero component
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

- Installed framer-motion dependency
- Added Hero1 component with 'Digital Command Center' branding
- Replaced MetrcDashboardPage header with new Hero component
This commit is contained in:
fullsizemalt 2025-12-19 17:03:53 -08:00
parent da5af8c288
commit 4a2533f121
5 changed files with 770 additions and 436 deletions

64
docs/AURA_UI_PLAN.md Normal file
View file

@ -0,0 +1,64 @@
# AuraUI Integration Plan and Audit
## Snapshot Info
- **Commit Hash**: `da5af8c` (Before AuraUI)
- **Rollback Command**: `git reset --hard da5af8c && git push -f origin main`
## Component Audit
We have selected the following components from the [AuraUI Library](https://auraui.com) to enhance the `ca-grow-ops-manager`:
### 1. 3D Digital Twin Hero (`feature-4-3d`)
- **Visuals**: Dark mode, floating 3D elements (cubes/spheres) with lime/purple accents.
- **Use Case**: **Facility 3D Viewer Loading/Landing State**.
- **Reason**: Perfectly aligns with the "Digital Twin" core pillar. Replaces the generic "Loading..." spinner with a technical, high-end visual.
### 2. High-Trust Hero (`hero-1`)
- **Visuals**: Centered H1, clear subtext, primary/secondary action buttons.
- **Use Case**: **Main Dashboard Header**.
- **Reason**: Provides immediate access to critical actions ("Emergency Stop", "Metrc Sync") and sets the tone for the application.
### 3. Split-Screen Auth Form (`signup-1`)
- **Visuals**: Left side form, Right side high-quality operational image.
- **Use Case**: **Visitor Kiosk Check-In** and **SOP Acknowledgement**.
- **Reason**: The "Compliance" aspect needs to feel authoritative and polished. The split view allows us to show legal text/images alongside the signature input.
### 4. Minimal Navbar (`header-6`)
- **Visuals**: Dark background, subtle hover effects, active state indicators.
- **Use Case**: **Global App Shell Navigation**.
- **Reason**: The current sidebar is functional but bulky. Moving to a sleek top-nav (or refined side-nav) using these styles cleans up screen real estate for the 3D viewer.
### 5. Bento Grid (`feature-14`)
- **Visuals**: Grid of varied-size cards, bento-box style.
- **Use Case**: **Operations Dashboard**.
- **Reason**: We have too many disparate metrics (Temp, Humidity, Task Count, Compliance Status). A bento grid unifies them into a single "Command Center" view.
## Integration Strategy
### Prerequisites (Checked)
- [x] `clsx` (Installed)
- [x] `tailwind-merge` (Installed)
- [x] `class-variance-authority` (Installed)
- [x] `lucide-react` (Installed)
- [ ] `framer-motion` (Missing - **Install Required**)
### Implementation Order
1. **Install `framer-motion`**: Required for AuraUI animations.
2. **Setup Utils**: Ensure `lib/utils.ts` has the standard `cn()` helper (it likely does, but verify).
3. **Implement `Hero-1`**: Replace the Dashboard header first as a low-risk, high-impact test.
4. **Implement `Feature-4-3d`**: Add to the 3D Viewer page.
### Rollback Plan
If the new components break the build or styling:
1. Revert to the snapshot commit.
2. Clean `node_modules` if `framer-motion` causes issues.

View file

@ -19,6 +19,7 @@
"browser-image-compression": "^2.0.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"framer-motion": "^12.23.26",
"i18next": "^25.7.2",
"i18next-browser-languagedetector": "^8.2.0",
"immer": "^11.0.1",
@ -5200,6 +5201,33 @@
"url": "https://github.com/sponsors/rawify"
}
},
"node_modules/framer-motion": {
"version": "12.23.26",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.26.tgz",
"integrity": "sha512-cPcIhgR42xBn1Uj+PzOyheMtZ73H927+uWPDVhUMqxy8UHt6Okavb6xIz9J/phFUHUj0OncR6UvMfJTXoc/LKA==",
"license": "MIT",
"dependencies": {
"motion-dom": "^12.23.23",
"motion-utils": "^12.23.6",
"tslib": "^2.4.0"
},
"peerDependencies": {
"@emotion/is-prop-valid": "*",
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/is-prop-valid": {
"optional": true
},
"react": {
"optional": true
},
"react-dom": {
"optional": true
}
}
},
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@ -6577,6 +6605,21 @@
"dev": true,
"license": "MIT"
},
"node_modules/motion-dom": {
"version": "12.23.23",
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.23.tgz",
"integrity": "sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA==",
"license": "MIT",
"dependencies": {
"motion-utils": "^12.23.6"
}
},
"node_modules/motion-utils": {
"version": "12.23.6",
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz",
"integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==",
"license": "MIT"
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",

View file

@ -22,6 +22,7 @@
"browser-image-compression": "^2.0.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"framer-motion": "^12.23.26",
"i18next": "^25.7.2",
"i18next-browser-languagedetector": "^8.2.0",
"immer": "^11.0.1",
@ -58,4 +59,4 @@
"vite": "^5.0.8",
"vitest": "^1.0.0"
}
}
}

View file

@ -0,0 +1,245 @@
import { useState, useEffect } from "react";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import {
Download,
Github,
Sparkles,
Loader,
Copy,
Check,
StopCircle,
Activity,
Leaf
} from "lucide-react";
import { Card, CardContent } from "@/components/ui/card";
import { cn } from "@/lib/utils";
/**
* AuraUI Hero 1 Component
* Customized for CA Grow Ops Manager
* Displays the main dashboard header with 'Digital Command Center' branding
*/
const Hero1 = () => {
const [isVisible, setIsVisible] = useState(false);
const [currentSentence, setCurrentSentence] = useState(0);
const [isAnimating, setIsAnimating] = useState(false);
const [showCorrected, setShowCorrected] = useState(false);
const [typingText, setTypingText] = useState("");
const [isTyping, setIsTyping] = useState(false);
const [isSelected, setIsSelected] = useState(false);
const [showPopup, setShowPopup] = useState(false);
// Customized demo text for Grow Ops context
const demoSentences = [
{
original: "Room 4 temp is 82 humidity 65 check veg lighting",
corrected:
"Room 4: Temp 82°F, Humidity 65%. Veg lighting inspection required.",
},
{
original: "feed batch 156 with heavy bloom schedule today",
corrected:
"Action: Feed Batch B-23-156. Schedule: Heavy Bloom. Date: Today.",
},
{
original: "visitor arrived for checkin name is john doe",
corrected:
"Visitor Log: John Doe checked in. NDA signature pending.",
},
];
useEffect(() => {
const timer = setTimeout(() => {
setIsVisible(true);
}, 300);
return () => clearTimeout(timer);
}, []);
useEffect(() => {
const startDemo = () => {
// Reset all states
setIsTyping(true);
setIsSelected(false);
setShowCorrected(false);
setTypingText("");
// Type the text character by character - much faster
const text = demoSentences[currentSentence].original;
let i = 0;
const typeInterval = setInterval(() => {
if (i < text.length) {
setTypingText(text.substring(0, i + 1));
i++;
} else {
clearInterval(typeInterval);
setIsTyping(false);
// Wait a bit, then select text
setTimeout(() => {
setIsSelected(true);
// Wait for selection, then start refining
setTimeout(() => {
setIsAnimating(true);
// Show refined text after processing
setTimeout(() => {
setShowCorrected(true);
setIsAnimating(false);
setIsSelected(false);
// Wait before next cycle
setTimeout(() => {
setCurrentSentence(
(prev) => (prev + 1) % demoSentences.length,
);
}, 2500);
}, 1800);
}, 1200);
}, 800);
}
}, 25); // Much faster typing speed
};
const interval = setInterval(startDemo, 8500);
startDemo(); // Start immediately
return () => clearInterval(interval);
}, [currentSentence]);
return (
<section className="relative w-full overflow-hidden bg-background">
{/* Extended background gradient with enhanced grid */}
<div className="cosmic-gradient cosmic-grid-enhanced min-h-[60vh] flex flex-col justify-center items-center relative py-20">
{/* Hero Content */}
<div
className={cn(
"relative z-10 max-w-5xl mx-auto px-6 text-center transition-all duration-700 transform",
isVisible ? "opacity-100 translate-y-0" : "opacity-0 translate-y-10"
)}
>
<div className="flex justify-center mb-6">
<Badge variant="secondary" className="gap-2 cosmic-glass">
<Activity className="h-3 w-3 animate-pulse text-primary" />
System Status: Nominal
<Sparkles className="h-3 w-3 text-yellow-400" />
</Badge>
</div>
<h1 className="text-4xl md:text-6xl lg:text-7xl font-bold tracking-tighter text-balance text-foreground mb-6 max-w-5xl">
Command Center for <span className="text-primary">Modern Cultivation</span>
</h1>
<p className="text-lg md:text-xl text-muted-foreground max-w-2xl mx-auto mb-8 leading-relaxed text-balance">
Real-time compliance, environmental monitoring, and task management in a single digital twin interface.
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center pt-2 mb-16">
<Button
size="lg"
className="gap-2 h-12 px-8"
onClick={() => { }} // Hook up to actual actions
>
<Leaf className="h-5 w-5" />
Metrc Sync
</Button>
<Button variant="outline" size="lg" className="gap-2 h-12 px-8">
<Activity className="h-5 w-5" />
View Alerts
</Button>
</div>
{/* Demo Box - Made taller */}
<div className="max-w-4xl mx-auto mb-8 cursor-default select-none pointer-events-none opacity-80 scale-90">
<div className="relative">
{/* Window-like demo box */}
<div className="bg-card/90 backdrop-blur-md rounded-xl border border-border/50 shadow-2xl overflow-hidden">
{/* Window header */}
<div className="bg-muted/30 px-4 py-3 border-b border-border/30 flex items-center gap-2">
<div className="flex gap-2">
<div className="w-3 h-3 rounded-full bg-red-500/70"></div>
<div className="w-3 h-3 rounded-full bg-yellow-500/70"></div>
<div className="w-3 h-3 rounded-full bg-green-500/70"></div>
</div>
<div className="text-sm text-muted-foreground ml-4">
AI Compliance Assistant
</div>
</div>
{/* Content area - Made taller */}
<div className="p-8 min-h-[120px] flex items-center justify-center">
<div className="w-full max-w-2xl">
<div
className={cn(
"text-lg leading-relaxed transition-all duration-500",
isAnimating ? "bg-accent/30 rounded-md p-3" : "",
isSelected ? "bg-primary/20 rounded-md p-3" : ""
)}
>
{showCorrected ? (
<span className="text-foreground font-mono">
{demoSentences[currentSentence].corrected}
</span>
) : (
<span className="text-muted-foreground font-mono">
{typingText}
{isTyping && <span className="animate-pulse">_</span>}
</span>
)}
</div>
{/* Status indicator */}
<div className="flex items-center gap-2 mt-4 text-xs font-mono uppercase tracking-widest">
{isAnimating ? (
<>
<Loader className="h-3 w-3 animate-spin text-primary" />
<span className="text-muted-foreground">
Processing SOP...
</span>
</>
) : showCorrected ? (
<>
<Check className="h-3 w-3 text-green-500" />
<span className="text-muted-foreground">
Compliance Verified
</span>
</>
) : isSelected ? (
<>
<Copy className="h-3 w-3 text-blue-500" />
<span className="text-muted-foreground">
Analyzing Input
</span>
</>
) : (
<>
<span className="text-muted-foreground">
Awaiting Voice/Text Command...
</span>
</>
)}
</div>
</div>
</div>
</div>
{/* Glow effect below the demo box - moved up and positioned better */}
<div className="absolute bottom-16 left-1/2 -translate-x-1/2 w-[600px] h-[120px] opacity-20 bg-primary blur-[80px] rounded-full -z-20"></div>
</div>
</div>
</div>
</div>
{showPopup && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
{/* Popup content */}
</div>
)}
</section>
);
};
export default Hero1;

View file

@ -7,6 +7,7 @@ import {
} from 'lucide-react';
import { metrcApi, MetrcLocation, MetrcDiscrepancy, MetrcReportResponse, MetrcAuditResponse } from '../lib/metrcApi';
import { PageHeader, EmptyState, CardSkeleton } from '../components/ui/LinearPrimitives';
import Hero1 from '../components/aura/Hero';
const SEVERITY_CONFIG = {
CRITICAL: { color: 'text-red-600 bg-red-100 dark:bg-red-900/30', label: 'Critical' },
@ -88,464 +89,444 @@ export default function MetrcDashboardPage() {
return (
<div className="space-y-6 pb-20 animate-in">
<PageHeader
title="METRC Integration"
subtitle="California Track-and-Trace Compliance"
actions={
<div className="flex gap-2">
<button
onClick={handleExportCsv}
className="btn btn-ghost"
title="Export CSV for manual METRC upload"
>
<Download size={16} />
<span className="hidden sm:inline">Export</span>
</button>
<button
onClick={handleSync}
disabled={syncing}
className="btn btn-primary"
>
{syncing ? (
<Loader2 size={16} className="animate-spin" />
) : (
<RefreshCw size={16} />
)}
<span className="hidden sm:inline">Sync</span>
</button>
</div>
}
/>
<Hero1 />
{/* Connection Status Banner */}
<div className={`card p-4 ${connectionStatus === 'demo' ? 'bg-amber-50 dark:bg-amber-900/20 border-amber-200 dark:border-amber-800' : 'bg-emerald-50 dark:bg-emerald-900/20 border-emerald-200 dark:border-emerald-800'}`}>
<div className="flex items-center gap-3">
{connectionStatus === 'demo' ? (
<>
<CloudOff className="text-amber-600 dark:text-amber-400" size={24} />
<div className="flex-1">
<p className="font-medium text-amber-800 dark:text-amber-200">Demo Mode</p>
<p className="text-sm text-amber-600 dark:text-amber-400">
METRC API not connected. Data shown is for demonstration purposes.
</p>
</div>
<a
href="https://api-ca.metrc.com/Documentation"
target="_blank"
rel="noopener noreferrer"
className="btn btn-ghost text-amber-700 dark:text-amber-300 text-sm"
<div className="px-6 space-y-6">
{/* Connection Status Banner */}
<div className={`card p-4 ${connectionStatus === 'demo' ? 'bg-amber-50 dark:bg-amber-900/20 border-amber-200 dark:border-amber-800' : 'bg-emerald-50 dark:bg-emerald-900/20 border-emerald-200 dark:border-emerald-800'}`}>
<div className="flex items-center gap-3">
{connectionStatus === 'demo' ? (
<>
<CloudOff className="text-amber-600 dark:text-amber-400" size={24} />
<div className="flex-1">
<p className="font-medium text-amber-800 dark:text-amber-200">Demo Mode</p>
<p className="text-sm text-amber-600 dark:text-amber-400">
METRC API not connected. Data shown is for demonstration purposes.
</p>
</div>
<a
href="https://api-ca.metrc.com/Documentation"
target="_blank"
rel="noopener noreferrer"
className="btn btn-ghost text-amber-700 dark:text-amber-300 text-sm"
>
<ExternalLink size={14} />
API Docs
</a>
</>
) : (
<>
<Cloud className="text-emerald-600 dark:text-emerald-400" size={24} />
<div className="flex-1">
<p className="font-medium text-emerald-800 dark:text-emerald-200">Connected to METRC</p>
<p className="text-sm text-emerald-600 dark:text-emerald-400">
Last sync: {lastSync ? lastSync.toLocaleString() : 'Never'}
</p>
</div>
<CheckCircle className="text-emerald-600" size={20} />
</>
)}
</div>
</div>
{/* Tab Navigation */}
<div className="flex gap-1 p-1 bg-secondary rounded-lg">
{
[
{ id: 'overview', label: 'Overview' },
{ id: 'plants', label: 'Plant Locations' },
{ id: 'audit', label: 'Audit Trail' }
].map(tab => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id as any)}
className={`flex-1 py-2 px-4 rounded-md text-sm font-medium transition-colors ${activeTab === tab.id
? 'bg-primary text-primary shadow-sm'
: 'text-secondary hover:text-primary'
}`}
>
<ExternalLink size={14} />
API Docs
</a>
</>
{tab.label}
</button>
))
}
</div >
{
loading ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4" >
{Array.from({ length: 4 }).map((_, i) => <CardSkeleton key={i} />)}
</div>
) : (
<>
<Cloud className="text-emerald-600 dark:text-emerald-400" size={24} />
<div className="flex-1">
<p className="font-medium text-emerald-800 dark:text-emerald-200">Connected to METRC</p>
<p className="text-sm text-emerald-600 dark:text-emerald-400">
Last sync: {lastSync ? lastSync.toLocaleString() : 'Never'}
</p>
</div>
<CheckCircle className="text-emerald-600" size={20} />
</>
)}
</div>
</div>
{/* Tab Navigation */}
<div className="flex gap-1 p-1 bg-secondary rounded-lg">
{[
{ id: 'overview', label: 'Overview' },
{ id: 'plants', label: 'Plant Locations' },
{ id: 'audit', label: 'Audit Trail' }
].map(tab => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id as any)}
className={`flex-1 py-2 px-4 rounded-md text-sm font-medium transition-colors ${activeTab === tab.id
? 'bg-primary text-primary shadow-sm'
: 'text-secondary hover:text-primary'
}`}
>
{tab.label}
</button>
))}
</div>
{loading ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{Array.from({ length: 4 }).map((_, i) => <CardSkeleton key={i} />)}
</div>
) : (
<>
{/* Overview Tab */}
{activeTab === 'overview' && (
<div className="space-y-6">
{/* Stats Grid */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
<div className="card p-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-emerald-100 dark:bg-emerald-900/30 flex items-center justify-center">
<MapPin className="text-emerald-600 dark:text-emerald-400" size={20} />
</div>
<div>
<p className="text-2xl font-bold text-primary">{report?.plantCount || 0}</p>
<p className="text-xs text-tertiary">Total Plants</p>
</div>
</div>
</div>
<div className="card p-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-blue-100 dark:bg-blue-900/30 flex items-center justify-center">
<ArrowUpDown className="text-blue-600 dark:text-blue-400" size={20} />
</div>
<div>
<p className="text-2xl font-bold text-primary">{audit?.summary.totalMoves || 0}</p>
<p className="text-xs text-tertiary">Plant Moves (30d)</p>
</div>
</div>
</div>
<div className="card p-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-green-100 dark:bg-green-900/30 flex items-center justify-center">
<CheckCircle className="text-green-600 dark:text-green-400" size={20} />
</div>
<div>
<p className="text-2xl font-bold text-primary">100%</p>
<p className="text-xs text-tertiary">Sync Rate</p>
</div>
</div>
</div>
<div className="card p-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-amber-100 dark:bg-amber-900/30 flex items-center justify-center">
<AlertTriangle className="text-amber-600 dark:text-amber-400" size={20} />
</div>
<div>
<p className="text-2xl font-bold text-primary">0</p>
<p className="text-xs text-tertiary">Discrepancies</p>
</div>
</div>
</div>
</div>
{/* Quick Actions */}
<div className="card p-4">
<h3 className="font-medium text-primary mb-4">Quick Actions</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3">
<button className="btn btn-ghost justify-start h-auto py-3" onClick={handleExportCsv}>
<Download size={18} className="text-accent" />
<div className="text-left">
<p className="font-medium">Export CSV</p>
<p className="text-xs text-tertiary">For manual METRC upload</p>
</div>
</button>
<button className="btn btn-ghost justify-start h-auto py-3" onClick={handleSync}>
<RefreshCw size={18} className="text-accent" />
<div className="text-left">
<p className="font-medium">Sync All Plants</p>
<p className="text-xs text-tertiary">Update all locations</p>
</div>
</button>
<button className="btn btn-ghost justify-start h-auto py-3" onClick={() => setActiveTab('audit')}>
<FileText size={18} className="text-accent" />
<div className="text-left">
<p className="font-medium">View Audit</p>
<p className="text-xs text-tertiary">Compliance report</p>
</div>
</button>
<a
href="https://ca.metrc.com"
target="_blank"
rel="noopener noreferrer"
className="btn btn-ghost justify-start h-auto py-3"
>
<ExternalLink size={18} className="text-accent" />
<div className="text-left">
<p className="font-medium">Open METRC</p>
<p className="text-xs text-tertiary">State portal</p>
</div>
</a>
</div>
</div>
{/* Recent Moves */}
{audit && audit.recentMoves.length > 0 && (
<div className="card">
<div className="p-4 border-b border-subtle">
<h3 className="font-medium text-primary">Recent Plant Moves</h3>
<p className="text-xs text-tertiary">Location changes requiring METRC update</p>
</div>
<div className="divide-y divide-subtle">
{audit.recentMoves.slice(0, 5).map((move: any, i: number) => (
<div key={i} className="p-4 flex items-center gap-4">
<div className="w-10 h-10 rounded-lg bg-blue-100 dark:bg-blue-900/30 flex items-center justify-center">
<ArrowUpDown size={18} className="text-blue-600 dark:text-blue-400" />
{/* Overview Tab */}
{activeTab === 'overview' && (
<div className="space-y-6">
{/* Stats Grid */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
<div className="card p-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-emerald-100 dark:bg-emerald-900/30 flex items-center justify-center">
<MapPin className="text-emerald-600 dark:text-emerald-400" size={20} />
</div>
<div className="flex-1 min-w-0">
<p className="font-medium text-primary text-sm">{move.plantTag}</p>
<p className="text-xs text-tertiary truncate">
{move.from} {move.to}
</p>
</div>
<div className="text-right">
<p className="text-xs text-tertiary">
{new Date(move.movedAt).toLocaleDateString()}
</p>
<div>
<p className="text-2xl font-bold text-primary">{report?.plantCount || 0}</p>
<p className="text-xs text-tertiary">Total Plants</p>
</div>
</div>
))}
</div>
<div className="card p-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-blue-100 dark:bg-blue-900/30 flex items-center justify-center">
<ArrowUpDown className="text-blue-600 dark:text-blue-400" size={20} />
</div>
<div>
<p className="text-2xl font-bold text-primary">{audit?.summary.totalMoves || 0}</p>
<p className="text-xs text-tertiary">Plant Moves (30d)</p>
</div>
</div>
</div>
<div className="card p-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-green-100 dark:bg-green-900/30 flex items-center justify-center">
<CheckCircle className="text-green-600 dark:text-green-400" size={20} />
</div>
<div>
<p className="text-2xl font-bold text-primary">100%</p>
<p className="text-xs text-tertiary">Sync Rate</p>
</div>
</div>
</div>
<div className="card p-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-amber-100 dark:bg-amber-900/30 flex items-center justify-center">
<AlertTriangle className="text-amber-600 dark:text-amber-400" size={20} />
</div>
<div>
<p className="text-2xl font-bold text-primary">0</p>
<p className="text-xs text-tertiary">Discrepancies</p>
</div>
</div>
</div>
</div>
</div>
)}
</div>
)}
{/* Plants Tab */}
{activeTab === 'plants' && (
<div className="space-y-4">
{/* Search */}
<div className="flex gap-3">
<div className="relative flex-1">
<Filter className="absolute left-3 top-1/2 -translate-y-1/2 text-tertiary" size={16} />
<input
type="text"
placeholder="Search by tag or room..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="input w-full pl-9"
/>
</div>
</div>
{/* Quick Actions */}
<div className="card p-4">
<h3 className="font-medium text-primary mb-4">Quick Actions</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3">
<button className="btn btn-ghost justify-start h-auto py-3" onClick={handleExportCsv}>
<Download size={18} className="text-accent" />
<div className="text-left">
<p className="font-medium">Export CSV</p>
<p className="text-xs text-tertiary">For manual METRC upload</p>
</div>
</button>
{/* Plant List */}
{filteredPlants.length === 0 ? (
<EmptyState
icon={MapPin}
title="No plants found"
description="No plant location data available."
/>
) : (
<div className="card overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead className="bg-secondary">
<tr>
<th className="text-left p-3 font-medium text-secondary">Tag</th>
<th className="text-left p-3 font-medium text-secondary">METRC Location</th>
<th className="text-left p-3 font-medium text-secondary">Room</th>
<th className="text-left p-3 font-medium text-secondary">Section</th>
<th className="text-left p-3 font-medium text-secondary">Position</th>
<th className="text-center p-3 font-medium text-secondary">Status</th>
<th className="text-right p-3 font-medium text-secondary">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-subtle">
{filteredPlants.slice(0, 50).map((plant: any) => (
<tr key={plant.plantId} className="hover:bg-secondary/50">
<td className="p-3 font-mono text-xs font-bold">{plant.tagNumber}</td>
<td className="p-3 font-mono text-xs text-accent">{plant.location || '-'}</td>
<td className="p-3">{plant.room}</td>
<td className="p-3">{plant.section || '-'}</td>
<td className="p-3">{plant.position || '-'}</td>
<td className="p-3 text-center">
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400">
<CheckCircle size={10} />
Synced
</span>
</td>
<td className="p-3 text-right">
<div className="flex items-center justify-end gap-2">
<button
onClick={() => setSelectedHistoryPlant(plant)}
className="btn btn-ghost btn-sm btn-square"
title="View History"
>
<Clock size={16} className="text-tertiary" />
</button>
<Link
to={`/facility/3d?plant=${plant.tagNumber}`}
className="btn btn-ghost btn-sm btn-square text-accent"
title="View in 3D"
>
<Box size={16} />
</Link>
</div>
</td>
</tr>
<button className="btn btn-ghost justify-start h-auto py-3" onClick={handleSync}>
<RefreshCw size={18} className="text-accent" />
<div className="text-left">
<p className="font-medium">Sync All Plants</p>
<p className="text-xs text-tertiary">Update all locations</p>
</div>
</button>
<button className="btn btn-ghost justify-start h-auto py-3" onClick={() => setActiveTab('audit')}>
<FileText size={18} className="text-accent" />
<div className="text-left">
<p className="font-medium">View Audit</p>
<p className="text-xs text-tertiary">Compliance report</p>
</div>
</button>
<a
href="https://ca.metrc.com"
target="_blank"
rel="noopener noreferrer"
className="btn btn-ghost justify-start h-auto py-3"
>
<ExternalLink size={18} className="text-accent" />
<div className="text-left">
<p className="font-medium">Open METRC</p>
<p className="text-xs text-tertiary">State portal</p>
</div>
</a>
</div>
</div>
{/* Recent Moves */}
{audit && audit.recentMoves.length > 0 && (
<div className="card">
<div className="p-4 border-b border-subtle">
<h3 className="font-medium text-primary">Recent Plant Moves</h3>
<p className="text-xs text-tertiary">Location changes requiring METRC update</p>
</div>
<div className="divide-y divide-subtle">
{audit.recentMoves.slice(0, 5).map((move: any, i: number) => (
<div key={i} className="p-4 flex items-center gap-4">
<div className="w-10 h-10 rounded-lg bg-blue-100 dark:bg-blue-900/30 flex items-center justify-center">
<ArrowUpDown size={18} className="text-blue-600 dark:text-blue-400" />
</div>
<div className="flex-1 min-w-0">
<p className="font-medium text-primary text-sm">{move.plantTag}</p>
<p className="text-xs text-tertiary truncate">
{move.from} {move.to}
</p>
</div>
<div className="text-right">
<p className="text-xs text-tertiary">
{new Date(move.movedAt).toLocaleDateString()}
</p>
</div>
</div>
))}
</tbody>
</table>
</div>
{filteredPlants.length > 50 && (
<div className="p-4 bg-secondary text-center text-sm text-tertiary">
Showing 50 of {filteredPlants.length} plants
</div>
</div>
)}
</div>
)}
</div>
)}
{/* Audit Tab */}
{activeTab === 'audit' && audit && (
<div className="space-y-6">
{/* Audit Summary */}
<div className="card p-4">
<div className="flex items-center justify-between mb-4">
{/* Plants Tab */}
{activeTab === 'plants' && (
<div className="space-y-4">
{/* Search */}
<div className="flex gap-3">
<div className="relative flex-1">
<Filter className="absolute left-3 top-1/2 -translate-y-1/2 text-tertiary" size={16} />
<input
type="text"
placeholder="Search by tag or room..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="input w-full pl-9"
/>
</div>
</div>
{/* Plant List */}
{filteredPlants.length === 0 ? (
<EmptyState
icon={MapPin}
title="No plants found"
description="No plant location data available."
/>
) : (
<div className="card overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead className="bg-secondary">
<tr>
<th className="text-left p-3 font-medium text-secondary">Tag</th>
<th className="text-left p-3 font-medium text-secondary">METRC Location</th>
<th className="text-left p-3 font-medium text-secondary">Room</th>
<th className="text-left p-3 font-medium text-secondary">Section</th>
<th className="text-left p-3 font-medium text-secondary">Position</th>
<th className="text-center p-3 font-medium text-secondary">Status</th>
<th className="text-right p-3 font-medium text-secondary">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-subtle">
{filteredPlants.slice(0, 50).map((plant: any) => (
<tr key={plant.plantId} className="hover:bg-secondary/50">
<td className="p-3 font-mono text-xs font-bold">{plant.tagNumber}</td>
<td className="p-3 font-mono text-xs text-accent">{plant.location || '-'}</td>
<td className="p-3">{plant.room}</td>
<td className="p-3">{plant.section || '-'}</td>
<td className="p-3">{plant.position || '-'}</td>
<td className="p-3 text-center">
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400">
<CheckCircle size={10} />
Synced
</span>
</td>
<td className="p-3 text-right">
<div className="flex items-center justify-end gap-2">
<button
onClick={() => setSelectedHistoryPlant(plant)}
className="btn btn-ghost btn-sm btn-square"
title="View History"
>
<Clock size={16} className="text-tertiary" />
</button>
<Link
to={`/facility/3d?plant=${plant.tagNumber}`}
className="btn btn-ghost btn-sm btn-square text-accent"
title="View in 3D"
>
<Box size={16} />
</Link>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
{filteredPlants.length > 50 && (
<div className="p-4 bg-secondary text-center text-sm text-tertiary">
Showing 50 of {filteredPlants.length} plants
</div>
)}
</div>
)}
</div>
)}
{/* Audit Tab */}
{activeTab === 'audit' && audit && (
<div className="space-y-6">
{/* Audit Summary */}
<div className="card p-4">
<div className="flex items-center justify-between mb-4">
<div>
<h3 className="font-medium text-primary">METRC Compliance Audit</h3>
<p className="text-xs text-tertiary">
{new Date(audit.dateRange.start).toLocaleDateString()} - {new Date(audit.dateRange.end).toLocaleDateString()}
</p>
</div>
<span className="badge badge-success">
<CheckCircle size={12} />
Compliant
</span>
</div>
<div className="grid grid-cols-3 gap-4 text-center">
<div className="p-3 bg-secondary rounded-lg">
<p className="text-2xl font-bold text-primary">{audit.summary.totalPlants}</p>
<p className="text-xs text-tertiary">Total Plants</p>
</div>
<div className="p-3 bg-secondary rounded-lg">
<p className="text-2xl font-bold text-primary">{audit.summary.totalMoves}</p>
<p className="text-xs text-tertiary">Location Changes</p>
</div>
<div className="p-3 bg-secondary rounded-lg">
<p className="text-2xl font-bold text-primary">{audit.summary.uniquePlantsMoved}</p>
<p className="text-xs text-tertiary">Plants Moved</p>
</div>
</div>
</div>
{/* Recent Moves Table */}
<div className="card overflow-hidden">
<div className="p-4 border-b border-subtle">
<h3 className="font-medium text-primary">Location Change History</h3>
</div>
{audit.recentMoves.length === 0 ? (
<div className="p-8 text-center text-tertiary">
<Clock size={32} className="mx-auto mb-2 opacity-50" />
<p>No location changes in this period</p>
</div>
) : (
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead className="bg-secondary">
<tr>
<th className="text-left p-3 font-medium text-secondary">Plant Tag</th>
<th className="text-left p-3 font-medium text-secondary">From</th>
<th className="text-left p-3 font-medium text-secondary">To</th>
<th className="text-left p-3 font-medium text-secondary">Date</th>
<th className="text-left p-3 font-medium text-secondary">Reason</th>
</tr>
</thead>
<tbody className="divide-y divide-subtle">
{audit.recentMoves.map((move: any, i: number) => (
<tr key={i} className="hover:bg-secondary/50">
<td className="p-3 font-mono text-xs">{move.plantTag}</td>
<td className="p-3 text-tertiary">{move.from}</td>
<td className="p-3 text-accent">{move.to}</td>
<td className="p-3">
{new Date(move.movedAt).toLocaleDateString()}
</td>
<td className="p-3 text-tertiary">{move.reason || '-'}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
</div>
)}
</>
)
}
{/* Plant History Modal */}
{
selectedHistoryPlant && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4 animate-in fade-in">
<div className="card w-full max-w-2xl max-h-[80vh] flex flex-col shadow-2xl animate-in zoom-in-95">
<div className="p-4 border-b border-subtle flex justify-between items-center">
<div>
<h3 className="font-medium text-primary">METRC Compliance Audit</h3>
<p className="text-xs text-tertiary">
{new Date(audit.dateRange.start).toLocaleDateString()} - {new Date(audit.dateRange.end).toLocaleDateString()}
</p>
<h3 className="text-lg font-bold text-primary">Plant History</h3>
<p className="text-sm text-accent font-mono">{selectedHistoryPlant.tagNumber}</p>
</div>
<span className="badge badge-success">
<CheckCircle size={12} />
Compliant
</span>
<button
onClick={() => setSelectedHistoryPlant(null)}
className="btn btn-ghost btn-sm btn-square"
>
<XCircle size={20} />
</button>
</div>
<div className="grid grid-cols-3 gap-4 text-center">
<div className="p-3 bg-secondary rounded-lg">
<p className="text-2xl font-bold text-primary">{audit.summary.totalPlants}</p>
<p className="text-xs text-tertiary">Total Plants</p>
<div className="p-0 overflow-y-auto flex-1">
{/* Current Status */}
<div className="p-4 bg-secondary border-b border-subtle grid grid-cols-2 gap-4 text-sm">
<div>
<p className="text-tertiary text-xs uppercase tracking-wider">Current Room</p>
<p className="font-medium text-primary">{selectedHistoryPlant.room}</p>
</div>
<div>
<p className="text-tertiary text-xs uppercase tracking-wider">Current Section</p>
<p className="font-medium text-primary">{selectedHistoryPlant.section || 'N/A'}</p>
</div>
</div>
<div className="p-3 bg-secondary rounded-lg">
<p className="text-2xl font-bold text-primary">{audit.summary.totalMoves}</p>
<p className="text-xs text-tertiary">Location Changes</p>
</div>
<div className="p-3 bg-secondary rounded-lg">
<p className="text-2xl font-bold text-primary">{audit.summary.uniquePlantsMoved}</p>
<p className="text-xs text-tertiary">Plants Moved</p>
</div>
</div>
</div>
{/* Recent Moves Table */}
<div className="card overflow-hidden">
<div className="p-4 border-b border-subtle">
<h3 className="font-medium text-primary">Location Change History</h3>
{/* Timeline */}
<div className="p-4">
<h4 className="text-sm font-medium text-tertiary mb-4 uppercase tracking-wider">Movement Log</h4>
{audit?.recentMoves.filter((m: any) => m.plantTag === selectedHistoryPlant.tagNumber).length === 0 ? (
<div className="text-center py-8 text-tertiary bg-secondary/30 rounded-lg">
<Clock className="mx-auto mb-2 opacity-30" />
<p>No recorded movements found for this plant.</p>
</div>
) : (
<ol className="relative border-s border-gray-200 dark:border-gray-700 ms-3 space-y-6">
{audit?.recentMoves
.filter((m: any) => m.plantTag === selectedHistoryPlant.tagNumber)
.sort((a: any, b: any) => new Date(b.movedAt).getTime() - new Date(a.movedAt).getTime())
.map((move: any, i: number) => (
<li key={i} className="mb-10 ms-4">
<div className="absolute w-3 h-3 bg-gray-200 rounded-full mt-1.5 -start-1.5 border border-white dark:border-gray-900 dark:bg-gray-700"></div>
<time className="mb-1 text-sm font-normal leading-none text-gray-400 dark:text-gray-500">
{new Date(move.movedAt).toLocaleString()}
</time>
<h3 className="text-sm font-semibold text-gray-900 dark:text-white mt-1">
Moved to <span className="text-accent">{move.to}</span>
</h3>
<p className="mb-4 text-sm font-normal text-gray-500 dark:text-gray-400">
From {move.from} Reason: {move.reason || 'Routine Maintenance'}
</p>
</li>
))}
</ol>
)}
</div>
</div>
<div className="p-4 border-t border-subtle bg-secondary/50 flex justify-end">
<Link
to={`/facility/3d?plant=${selectedHistoryPlant.tagNumber}`}
className="btn btn-primary btn-sm"
>
<Box size={16} className="mr-2" />
Locate in 3D
</Link>
</div>
{audit.recentMoves.length === 0 ? (
<div className="p-8 text-center text-tertiary">
<Clock size={32} className="mx-auto mb-2 opacity-50" />
<p>No location changes in this period</p>
</div>
) : (
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead className="bg-secondary">
<tr>
<th className="text-left p-3 font-medium text-secondary">Plant Tag</th>
<th className="text-left p-3 font-medium text-secondary">From</th>
<th className="text-left p-3 font-medium text-secondary">To</th>
<th className="text-left p-3 font-medium text-secondary">Date</th>
<th className="text-left p-3 font-medium text-secondary">Reason</th>
</tr>
</thead>
<tbody className="divide-y divide-subtle">
{audit.recentMoves.map((move: any, i: number) => (
<tr key={i} className="hover:bg-secondary/50">
<td className="p-3 font-mono text-xs">{move.plantTag}</td>
<td className="p-3 text-tertiary">{move.from}</td>
<td className="p-3 text-accent">{move.to}</td>
<td className="p-3">
{new Date(move.movedAt).toLocaleDateString()}
</td>
<td className="p-3 text-tertiary">{move.reason || '-'}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
</div>
)}
</>
)}
{/* Plant History Modal */}
{selectedHistoryPlant && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4 animate-in fade-in">
<div className="card w-full max-w-2xl max-h-[80vh] flex flex-col shadow-2xl animate-in zoom-in-95">
<div className="p-4 border-b border-subtle flex justify-between items-center">
<div>
<h3 className="text-lg font-bold text-primary">Plant History</h3>
<p className="text-sm text-accent font-mono">{selectedHistoryPlant.tagNumber}</p>
</div>
<button
onClick={() => setSelectedHistoryPlant(null)}
className="btn btn-ghost btn-sm btn-square"
>
<XCircle size={20} />
</button>
</div>
<div className="p-0 overflow-y-auto flex-1">
{/* Current Status */}
<div className="p-4 bg-secondary border-b border-subtle grid grid-cols-2 gap-4 text-sm">
<div>
<p className="text-tertiary text-xs uppercase tracking-wider">Current Room</p>
<p className="font-medium text-primary">{selectedHistoryPlant.room}</p>
</div>
<div>
<p className="text-tertiary text-xs uppercase tracking-wider">Current Section</p>
<p className="font-medium text-primary">{selectedHistoryPlant.section || 'N/A'}</p>
</div>
</div>
{/* Timeline */}
<div className="p-4">
<h4 className="text-sm font-medium text-tertiary mb-4 uppercase tracking-wider">Movement Log</h4>
{audit?.recentMoves.filter((m: any) => m.plantTag === selectedHistoryPlant.tagNumber).length === 0 ? (
<div className="text-center py-8 text-tertiary bg-secondary/30 rounded-lg">
<Clock className="mx-auto mb-2 opacity-30" />
<p>No recorded movements found for this plant.</p>
</div>
) : (
<ol className="relative border-s border-gray-200 dark:border-gray-700 ms-3 space-y-6">
{audit?.recentMoves
.filter((m: any) => m.plantTag === selectedHistoryPlant.tagNumber)
.sort((a: any, b: any) => new Date(b.movedAt).getTime() - new Date(a.movedAt).getTime())
.map((move: any, i: number) => (
<li key={i} className="mb-10 ms-4">
<div className="absolute w-3 h-3 bg-gray-200 rounded-full mt-1.5 -start-1.5 border border-white dark:border-gray-900 dark:bg-gray-700"></div>
<time className="mb-1 text-sm font-normal leading-none text-gray-400 dark:text-gray-500">
{new Date(move.movedAt).toLocaleString()}
</time>
<h3 className="text-sm font-semibold text-gray-900 dark:text-white mt-1">
Moved to <span className="text-accent">{move.to}</span>
</h3>
<p className="mb-4 text-sm font-normal text-gray-500 dark:text-gray-400">
From {move.from} Reason: {move.reason || 'Routine Maintenance'}
</p>
</li>
))}
</ol>
)}
</div>
</div>
<div className="p-4 border-t border-subtle bg-secondary/50 flex justify-end">
<Link
to={`/facility/3d?plant=${selectedHistoryPlant.tagNumber}`}
className="btn btn-primary btn-sm"
>
<Box size={16} className="mr-2" />
Locate in 3D
</Link>
</div>
</div>
</div>
)}
</div>
);
)
}
</div >
);
}