feat(ui): Integrate AuraUI Hero component
- Installed framer-motion dependency - Added Hero1 component with 'Digital Command Center' branding - Replaced MetrcDashboardPage header with new Hero component
This commit is contained in:
parent
da5af8c288
commit
4a2533f121
5 changed files with 770 additions and 436 deletions
64
docs/AURA_UI_PLAN.md
Normal file
64
docs/AURA_UI_PLAN.md
Normal 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.
|
||||
43
frontend/package-lock.json
generated
43
frontend/package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
245
frontend/src/components/aura/Hero.tsx
Normal file
245
frontend/src/components/aura/Hero.tsx
Normal 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;
|
||||
|
|
@ -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 >
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue