mirror of
https://github.com/ForFarmTeam/ForFarm.git
synced 2025-12-19 14:04:08 +01:00
ui: improve crops detailed page
This commit is contained in:
parent
4b772e20d0
commit
b1c8b50a86
171
frontend/api/farm.ts
Normal file
171
frontend/api/farm.ts
Normal file
@ -0,0 +1,171 @@
|
||||
import type { Crop, CropAnalytics, Farm } from "@/types";
|
||||
|
||||
/**
|
||||
* Fetch mock crop data by id.
|
||||
* @param id - The crop identifier.
|
||||
* @returns A promise that resolves to a Crop object.
|
||||
*/
|
||||
export async function fetchCropById(id: string): Promise<Crop> {
|
||||
// Simulate an API delay if needed.
|
||||
return Promise.resolve({
|
||||
id,
|
||||
farmId: "1",
|
||||
name: "Monthong Durian",
|
||||
plantedDate: new Date("2024-01-15"),
|
||||
status: "growing",
|
||||
variety: "Premium Grade",
|
||||
expectedHarvest: new Date("2024-07-15"),
|
||||
area: "2.5 hectares",
|
||||
healthScore: 85,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch mock crop analytics data by crop id.
|
||||
* @param id - The crop identifier.
|
||||
* @returns A promise that resolves to a CropAnalytics object.
|
||||
*/
|
||||
export async function fetchAnalyticsByCropId(id: string): Promise<CropAnalytics> {
|
||||
return Promise.resolve({
|
||||
cropId: id,
|
||||
growthProgress: 45,
|
||||
humidity: 75,
|
||||
temperature: 28,
|
||||
sunlight: 85,
|
||||
waterLevel: 65,
|
||||
plantHealth: "good",
|
||||
nextAction: "Water the plant",
|
||||
nextActionDue: new Date("2024-02-15"),
|
||||
soilMoisture: 70,
|
||||
windSpeed: "12 km/h",
|
||||
rainfall: "25mm last week",
|
||||
nutrientLevels: {
|
||||
nitrogen: 80,
|
||||
phosphorus: 65,
|
||||
potassium: 75,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulates an API call to fetch farms.
|
||||
* Introduces a delay and a random error to emulate network conditions.
|
||||
*
|
||||
* @returns A promise that resolves to an array of Farm objects.
|
||||
*/
|
||||
export async function fetchFarms(): Promise<Farm[]> {
|
||||
// Simulate network delay
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
|
||||
// Simulate a random error (roughly 1 in 10 chance)
|
||||
if (Math.random() < 0.1) {
|
||||
throw new Error("Failed to fetch farms. Please try again later.");
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
id: "1",
|
||||
name: "Green Valley Farm",
|
||||
location: "Bangkok",
|
||||
type: "durian",
|
||||
createdAt: new Date("2023-01-01"),
|
||||
area: "12.5 hectares",
|
||||
crops: 5,
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "Sunrise Orchard",
|
||||
location: "Chiang Mai",
|
||||
type: "mango",
|
||||
createdAt: new Date("2023-02-15"),
|
||||
area: "8.3 hectares",
|
||||
crops: 3,
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
name: "Golden Harvest Fields",
|
||||
location: "Phuket",
|
||||
type: "rice",
|
||||
createdAt: new Date("2023-03-22"),
|
||||
area: "20.1 hectares",
|
||||
crops: 2,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulates an API call to fetch farm details along with its crops.
|
||||
* This function adds a delay and randomly generates an error to mimic real-world conditions.
|
||||
*
|
||||
* @param farmId - The unique identifier of the farm to retrieve.
|
||||
* @returns A promise resolving with an object that contains the farm details and an array of crops.
|
||||
* @throws An error if the simulated network call fails or if the farm is not found.
|
||||
*/
|
||||
export async function fetchFarmDetails(farmId: string): Promise<{ farm: Farm; crops: Crop[] }> {
|
||||
// Simulate network delay
|
||||
await new Promise((resolve) => setTimeout(resolve, 1200));
|
||||
|
||||
// Randomly simulate an error (about 1 in 10 chance)
|
||||
if (Math.random() < 0.1) {
|
||||
throw new Error("Failed to fetch farm details. Please try again later.");
|
||||
}
|
||||
|
||||
// Simulate a not found error if the given farmId is "999"
|
||||
if (farmId === "999") {
|
||||
throw new Error("FARM_NOT_FOUND");
|
||||
}
|
||||
|
||||
const farm: Farm = {
|
||||
id: farmId,
|
||||
name: "Green Valley Farm",
|
||||
location: "Bangkok, Thailand",
|
||||
type: "durian",
|
||||
createdAt: new Date("2023-01-15"),
|
||||
area: "12.5 hectares",
|
||||
crops: 3,
|
||||
weather: {
|
||||
temperature: 28,
|
||||
humidity: 75,
|
||||
rainfall: "25mm last week",
|
||||
sunlight: 85,
|
||||
},
|
||||
};
|
||||
|
||||
const crops: Crop[] = [
|
||||
{
|
||||
id: "1",
|
||||
farmId,
|
||||
name: "Monthong Durian",
|
||||
plantedDate: new Date("2023-03-15"),
|
||||
status: "growing",
|
||||
variety: "Premium",
|
||||
area: "4.2 hectares",
|
||||
healthScore: 92,
|
||||
progress: 65,
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
farmId,
|
||||
name: "Chanee Durian",
|
||||
plantedDate: new Date("2023-02-20"),
|
||||
status: "planned",
|
||||
variety: "Standard",
|
||||
area: "3.8 hectares",
|
||||
healthScore: 0,
|
||||
progress: 0,
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
farmId,
|
||||
name: "Kradum Durian",
|
||||
plantedDate: new Date("2022-11-05"),
|
||||
status: "harvested",
|
||||
variety: "Premium",
|
||||
area: "4.5 hectares",
|
||||
healthScore: 100,
|
||||
progress: 100,
|
||||
},
|
||||
];
|
||||
|
||||
return { farm, crops };
|
||||
}
|
||||
@ -16,7 +16,7 @@ interface AnalyticsDialogProps {
|
||||
export function AnalyticsDialog({ open, onOpenChange, crop, analytics }: AnalyticsDialogProps) {
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-[800px]">
|
||||
<DialogContent className="sm:max-w-[800px] dark:bg-background">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Crop Analytics - {crop.name}</DialogTitle>
|
||||
</DialogHeader>
|
||||
@ -30,30 +30,30 @@ export function AnalyticsDialog({ open, onOpenChange, crop, analytics }: Analyti
|
||||
|
||||
<TabsContent value="overview" className="space-y-4">
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
<Card>
|
||||
<Card className="dark:bg-slate-800">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Growth Rate</CardTitle>
|
||||
<Sprout className="h-4 w-4 text-muted-foreground" />
|
||||
<Sprout className="h-4 w-4 text-muted-foreground dark:text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">+2.5%</div>
|
||||
<p className="text-xs text-muted-foreground">+20.1% from last week</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<Card className="dark:bg-slate-800">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Water Usage</CardTitle>
|
||||
<Droplets className="h-4 w-4 text-muted-foreground" />
|
||||
<Droplets className="h-4 w-4 text-muted-foreground dark:text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">15.2L</div>
|
||||
<p className="text-xs text-muted-foreground">per day average</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<Card className="dark:bg-slate-800">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Sunlight</CardTitle>
|
||||
<Sun className="h-4 w-4 text-muted-foreground" />
|
||||
<Sun className="h-4 w-4 text-muted-foreground dark:text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{analytics.sunlight}%</div>
|
||||
@ -62,7 +62,7 @@ export function AnalyticsDialog({ open, onOpenChange, crop, analytics }: Analyti
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<Card className="dark:bg-slate-800">
|
||||
<CardHeader>
|
||||
<CardTitle>Growth Timeline</CardTitle>
|
||||
<CardDescription>Daily growth rate over time</CardDescription>
|
||||
@ -75,7 +75,7 @@ export function AnalyticsDialog({ open, onOpenChange, crop, analytics }: Analyti
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="growth" className="space-y-4">
|
||||
<Card>
|
||||
<Card className="dark:bg-slate-800">
|
||||
<CardHeader>
|
||||
<CardTitle>Detailed Growth Analysis</CardTitle>
|
||||
<CardDescription>Comprehensive growth metrics</CardDescription>
|
||||
@ -87,7 +87,7 @@ export function AnalyticsDialog({ open, onOpenChange, crop, analytics }: Analyti
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="environment" className="space-y-4">
|
||||
<Card>
|
||||
<Card className="dark:bg-slate-800">
|
||||
<CardHeader>
|
||||
<CardTitle>Environmental Conditions</CardTitle>
|
||||
<CardDescription>Temperature, humidity, and more</CardDescription>
|
||||
|
||||
@ -43,11 +43,11 @@ export function ChatbotDialog({ open, onOpenChange, cropName }: ChatbotDialogPro
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<VisuallyHidden>
|
||||
<DialogTitle></DialogTitle>
|
||||
<DialogTitle>Farming Assistant Chat</DialogTitle>
|
||||
</VisuallyHidden>
|
||||
<DialogContent className="sm:max-w-[500px] p-0">
|
||||
<DialogContent className="sm:max-w-[500px] p-0 dark:bg-background">
|
||||
<div className="flex flex-col h-[600px]">
|
||||
<div className="p-4 border-b">
|
||||
<div className="p-4 border-b dark:border-slate-700">
|
||||
<h2 className="text-lg font-semibold">Farming Assistant</h2>
|
||||
<p className="text-sm text-muted-foreground">Ask questions about your {cropName}</p>
|
||||
</div>
|
||||
@ -58,7 +58,9 @@ export function ChatbotDialog({ open, onOpenChange, cropName }: ChatbotDialogPro
|
||||
<div key={i} className={`flex ${message.role === "user" ? "justify-end" : "justify-start"}`}>
|
||||
<div
|
||||
className={`rounded-lg px-4 py-2 max-w-[80%] ${
|
||||
message.role === "user" ? "bg-primary text-primary-foreground" : "bg-muted"
|
||||
message.role === "user"
|
||||
? "bg-primary text-primary-foreground dark:bg-primary dark:text-primary-foreground"
|
||||
: "bg-muted dark:bg-muted dark:text-muted-foreground"
|
||||
}`}>
|
||||
{message.content}
|
||||
</div>
|
||||
@ -67,7 +69,7 @@ export function ChatbotDialog({ open, onOpenChange, cropName }: ChatbotDialogPro
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
<div className="p-4 border-t">
|
||||
<div className="p-4 border-t dark:border-slate-700">
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
|
||||
@ -1,255 +1,407 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import {
|
||||
ArrowLeft,
|
||||
MapPin,
|
||||
Sprout,
|
||||
LineChart,
|
||||
MessageSquare,
|
||||
Settings,
|
||||
AlertCircle,
|
||||
Droplets,
|
||||
Sun,
|
||||
ThermometerSun,
|
||||
Timer,
|
||||
ListCollapse,
|
||||
Calendar,
|
||||
Leaf,
|
||||
CloudRain,
|
||||
Wind,
|
||||
} from "lucide-react";
|
||||
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { ChatbotDialog } from "./chatbot-dialog";
|
||||
import { AnalyticsDialog } from "./analytics-dialog";
|
||||
import { HoverCard, HoverCardContent, HoverCardTrigger } from "@/components/ui/hover-card";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import type { Crop, CropAnalytics } from "@/types";
|
||||
import GoogleMapWithDrawing from "@/components/google-map-with-drawing";
|
||||
import { fetchCropById, fetchAnalyticsByCropId } from "@/api/farm";
|
||||
|
||||
const getCropById = (id: string): Crop => {
|
||||
return {
|
||||
id,
|
||||
farmId: "1",
|
||||
name: "Monthong Durian",
|
||||
plantedDate: new Date("2024-01-15"),
|
||||
status: "growing",
|
||||
};
|
||||
};
|
||||
|
||||
const getAnalyticsByCropId = (id: string): CropAnalytics => {
|
||||
return {
|
||||
cropId: id,
|
||||
growthProgress: 45, // Percentage
|
||||
humidity: 75, // Percentage
|
||||
temperature: 28, // °C
|
||||
sunlight: 85, // Percentage
|
||||
waterLevel: 65, // Percentage
|
||||
plantHealth: "good", // "good", "warning", "critical"
|
||||
nextAction: "Water the plant",
|
||||
nextActionDue: new Date("2024-02-15"),
|
||||
};
|
||||
};
|
||||
|
||||
export default function CropDetailPage({ params }: { params: Promise<{ farmId: string; cropId: string }> }) {
|
||||
const { farmId, cropId } = React.use(params);
|
||||
interface CropDetailPageParams {
|
||||
farmId: string;
|
||||
cropId: string;
|
||||
}
|
||||
|
||||
export default function CropDetailPage({ params }: { params: Promise<CropDetailPageParams> }) {
|
||||
const router = useRouter();
|
||||
const [crop] = useState(getCropById(cropId));
|
||||
const analytics = getAnalyticsByCropId(cropId);
|
||||
const [crop, setCrop] = useState<Crop | null>(null);
|
||||
const [analytics, setAnalytics] = useState<CropAnalytics | null>(null);
|
||||
const [isChatOpen, setIsChatOpen] = useState(false);
|
||||
const [isAnalyticsOpen, setIsAnalyticsOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchData() {
|
||||
const resolvedParams = await params;
|
||||
const cropData = await fetchCropById(resolvedParams.cropId);
|
||||
const analyticsData = await fetchAnalyticsByCropId(resolvedParams.cropId);
|
||||
setCrop(cropData);
|
||||
setAnalytics(analyticsData);
|
||||
}
|
||||
fetchData();
|
||||
}, [params]);
|
||||
|
||||
if (!crop || !analytics) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-background text-foreground">Loading...</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Colors for plant health badge.
|
||||
const healthColors = {
|
||||
good: "text-green-500",
|
||||
warning: "text-yellow-500",
|
||||
critical: "text-red-500",
|
||||
good: "text-green-500 bg-green-50 dark:bg-green-900",
|
||||
warning: "text-yellow-500 bg-yellow-50 dark:bg-yellow-900",
|
||||
critical: "text-red-500 bg-red-50 dark:bg-red-900",
|
||||
};
|
||||
|
||||
const actions = [
|
||||
const quickActions = [
|
||||
{
|
||||
title: "Analytics",
|
||||
icon: LineChart,
|
||||
description: "View detailed growth analytics",
|
||||
onClick: () => setIsAnalyticsOpen(true),
|
||||
color: "bg-blue-50 dark:bg-blue-900 text-blue-600 dark:text-blue-300",
|
||||
},
|
||||
{
|
||||
title: "Chat Assistant",
|
||||
icon: MessageSquare,
|
||||
description: "Get help and advice",
|
||||
onClick: () => setIsChatOpen(true),
|
||||
color: "bg-green-50 dark:bg-green-900 text-green-600 dark:text-green-300",
|
||||
},
|
||||
{
|
||||
title: "Detailed",
|
||||
title: "Crop Details",
|
||||
icon: ListCollapse,
|
||||
description: "View detailed of crop",
|
||||
onClick: () => console.log("Detailed clicked"),
|
||||
description: "View detailed information",
|
||||
onClick: () => console.log("Details clicked"),
|
||||
color: "bg-purple-50 dark:bg-purple-900 text-purple-600 dark:text-purple-300",
|
||||
},
|
||||
{
|
||||
title: "Settings",
|
||||
icon: Settings,
|
||||
description: "Configure crop settings",
|
||||
onClick: () => console.log("Settings clicked"),
|
||||
},
|
||||
{
|
||||
title: "Report Issue",
|
||||
icon: AlertCircle,
|
||||
description: "Report a problem",
|
||||
onClick: () => console.log("Report clicked"),
|
||||
color: "bg-gray-50 dark:bg-gray-900 text-gray-600 dark:text-gray-300",
|
||||
},
|
||||
];
|
||||
|
||||
const [isChatOpen, setIsChatOpen] = useState(false);
|
||||
const [isAnalyticsOpen, setIsAnalyticsOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="container max-w-screen-xl p-8">
|
||||
<Button variant="ghost" className="mb-4" onClick={() => router.back()}>
|
||||
<ArrowLeft className="mr-2 h-4 w-4" /> Back to Farm
|
||||
</Button>
|
||||
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
{/* Left Column - Crop Details */}
|
||||
<div className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="h-8 w-8 rounded-full bg-primary/10 flex items-center justify-center">
|
||||
<Sprout className="h-4 w-4 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">{crop.name}</h1>
|
||||
<p className="text-sm text-muted-foreground">Planted on {crop.plantedDate.toLocaleDateString()}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Badge variant="outline" className={healthColors[analytics.plantHealth]}>
|
||||
{analytics.plantHealth.toUpperCase()}
|
||||
</Badge>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground mb-2">Growth Progress</p>
|
||||
<Progress value={analytics.growthProgress} className="h-2" />
|
||||
<p className="text-sm text-muted-foreground mt-1">{analytics.growthProgress}% Complete</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Droplets className="h-4 w-4 text-blue-500" />
|
||||
<span className="text-sm">Humidity</span>
|
||||
<div className="min-h-screen bg-background text-foreground">
|
||||
<div className="container max-w-7xl p-6 mx-auto">
|
||||
{/* Header */}
|
||||
<div className="flex flex-col gap-6 mb-8">
|
||||
<div className="flex items-center justify-between">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="gap-2 text-green-700 dark:text-green-300 hover:text-green-800 dark:hover:text-green-200 hover:bg-green-100/50 dark:hover:bg-green-800/50"
|
||||
onClick={() => router.back()}>
|
||||
<ArrowLeft className="h-4 w-4" /> Back to Farm
|
||||
</Button>
|
||||
<HoverCard>
|
||||
<HoverCardTrigger asChild>
|
||||
<Button variant="outline" className="gap-2">
|
||||
<Calendar className="h-4 w-4" /> Timeline
|
||||
</Button>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent className="w-80">
|
||||
<div className="flex justify-between space-x-4">
|
||||
<Avatar>
|
||||
<AvatarImage src="/placeholder.svg" />
|
||||
<AvatarFallback>
|
||||
<Sprout className="h-4 w-4" />
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="space-y-1">
|
||||
<h4 className="text-sm font-semibold">Growth Timeline</h4>
|
||||
<p className="text-sm text-muted-foreground">Planted on {crop.plantedDate.toLocaleDateString()}</p>
|
||||
<div className="flex items-center pt-2">
|
||||
<Separator className="w-full" />
|
||||
<span className="mx-2 text-xs text-muted-foreground">
|
||||
{Math.floor(analytics.growthProgress)}% Complete
|
||||
</span>
|
||||
<Separator className="w-full" />
|
||||
</div>
|
||||
<p className="text-2xl font-semibold">{analytics.humidity}%</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<ThermometerSun className="h-4 w-4 text-orange-500" />
|
||||
<span className="text-sm">Temperature</span>
|
||||
</div>
|
||||
<p className="text-2xl font-semibold">{analytics.temperature}°C</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Sun className="h-4 w-4 text-yellow-500" />
|
||||
<span className="text-sm">Sunlight</span>
|
||||
</div>
|
||||
<p className="text-2xl font-semibold">{analytics.sunlight}%</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Droplets className="h-4 w-4 text-blue-500" />
|
||||
<span className="text-sm">Water Level</span>
|
||||
</div>
|
||||
<p className="text-2xl font-semibold">{analytics.waterLevel}%</p>
|
||||
</div>
|
||||
</div>
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Timer className="h-4 w-4 text-primary" />
|
||||
<span className="text-sm font-medium">Next Action Required</span>
|
||||
</div>
|
||||
<div className="bg-muted/50 p-4 rounded-lg">
|
||||
<p className="font-medium">{analytics.nextAction}</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Due by {analytics.nextActionDue.toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col md:flex-row justify-between gap-4">
|
||||
<div className="space-y-1">
|
||||
<h1 className="text-3xl font-bold tracking-tight">{crop.name}</h1>
|
||||
<p className="text-muted-foreground">
|
||||
{crop.variety} • {crop.area}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex flex-col items-end">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline" className={`${healthColors[analytics.plantHealth]} border`}>
|
||||
Health Score: {crop.healthScore}%
|
||||
</Badge>
|
||||
<Badge variant="outline" className="bg-blue-50 dark:bg-blue-900 text-blue-600 dark:text-blue-300">
|
||||
Growing
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<h2 className="text-lg font-semibold">Actions</h2>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{actions.map((action) => (
|
||||
<Button
|
||||
key={action.title}
|
||||
variant="outline"
|
||||
className="h-auto p-4 flex flex-col items-center gap-2"
|
||||
onClick={action.onClick}>
|
||||
<action.icon className="h-6 w-6" />
|
||||
<span className="font-medium">{action.title}</span>
|
||||
<span className="text-xs text-muted-foreground text-center">{action.description}</span>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<Card className="h-[400px] mb-32">
|
||||
<CardContent className="p-0 h-full">
|
||||
<GoogleMapWithDrawing />
|
||||
<div className="h-full w-full bg-muted/20 flex items-center justify-center">
|
||||
{/* <div className="text-center space-y-2">
|
||||
<MapPin className="h-8 w-8 mx-auto text-muted-foreground" />
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Map placeholder
|
||||
<br />
|
||||
Click to view full map
|
||||
{crop.expectedHarvest ? (
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
Expected harvest: {crop.expectedHarvest.toLocaleDateString()}
|
||||
</p>
|
||||
</div> */}
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground mt-1">Expected harvest date not available</p>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<h2 className="text-lg font-semibold">Quick Analytics</h2>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Tabs defaultValue="growth">
|
||||
<TabsList className="grid w-full grid-cols-3">
|
||||
<TabsTrigger value="growth">Growth</TabsTrigger>
|
||||
<TabsTrigger value="health">Health</TabsTrigger>
|
||||
<TabsTrigger value="water">Water</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="growth" className="flex items-center justify-center text-muted-foreground">
|
||||
Growth chart placeholder
|
||||
</TabsContent>
|
||||
<TabsContent value="health" className="flex items-center justify-center text-muted-foreground">
|
||||
Health metrics placeholder
|
||||
</TabsContent>
|
||||
<TabsContent value="water" className="flex items-center justify-center text-muted-foreground">
|
||||
Water usage placeholder
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="grid gap-6 md:grid-cols-12">
|
||||
{/* Left Column */}
|
||||
<div className="md:col-span-8 space-y-6">
|
||||
{/* Quick Actions */}
|
||||
<div className="grid sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{quickActions.map((action) => (
|
||||
<Button
|
||||
key={action.title}
|
||||
variant="outline"
|
||||
className={`h-auto p-4 flex flex-col items-center gap-3 transition-all group ${action.color} hover:scale-105`}
|
||||
onClick={action.onClick}>
|
||||
<div className={`p-3 rounded-lg ${action.color} group-hover:scale-110 transition-transform`}>
|
||||
<action.icon className="h-5 w-5" />
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="font-medium mb-1">{action.title}</div>
|
||||
<p className="text-xs text-muted-foreground">{action.description}</p>
|
||||
</div>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Environmental Metrics */}
|
||||
<Card className="border-green-100 dark:border-green-700">
|
||||
<CardHeader>
|
||||
<CardTitle>Environmental Conditions</CardTitle>
|
||||
<CardDescription>Real-time monitoring of growing conditions</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid gap-6">
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
{[
|
||||
{
|
||||
icon: ThermometerSun,
|
||||
label: "Temperature",
|
||||
value: `${analytics.temperature}°C`,
|
||||
color: "text-orange-500 dark:text-orange-300",
|
||||
bg: "bg-orange-50 dark:bg-orange-900",
|
||||
},
|
||||
{
|
||||
icon: Droplets,
|
||||
label: "Humidity",
|
||||
value: `${analytics.humidity}%`,
|
||||
color: "text-blue-500 dark:text-blue-300",
|
||||
bg: "bg-blue-50 dark:bg-blue-900",
|
||||
},
|
||||
{
|
||||
icon: Sun,
|
||||
label: "Sunlight",
|
||||
value: `${analytics.sunlight}%`,
|
||||
color: "text-yellow-500 dark:text-yellow-300",
|
||||
bg: "bg-yellow-50 dark:bg-yellow-900",
|
||||
},
|
||||
{
|
||||
icon: Leaf,
|
||||
label: "Soil Moisture",
|
||||
value: `${analytics.soilMoisture}%`,
|
||||
color: "text-green-500 dark:text-green-300",
|
||||
bg: "bg-green-50 dark:bg-green-900",
|
||||
},
|
||||
{
|
||||
icon: Wind,
|
||||
label: "Wind Speed",
|
||||
value: analytics.windSpeed,
|
||||
color: "text-gray-500 dark:text-gray-300",
|
||||
bg: "bg-gray-50 dark:bg-gray-900",
|
||||
},
|
||||
{
|
||||
icon: CloudRain,
|
||||
label: "Rainfall",
|
||||
value: analytics.rainfall,
|
||||
color: "text-indigo-500 dark:text-indigo-300",
|
||||
bg: "bg-indigo-50 dark:bg-indigo-900",
|
||||
},
|
||||
].map((metric) => (
|
||||
<Card
|
||||
key={metric.label}
|
||||
className="border-none shadow-none bg-gradient-to-br from-white to-gray-50/50 dark:from-slate-800 dark:to-slate-700/50">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className={`p-2 rounded-lg ${metric.bg}`}>
|
||||
<metric.icon className={`h-4 w-4 ${metric.color}`} />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-muted-foreground">{metric.label}</p>
|
||||
<p className="text-2xl font-semibold tracking-tight">{metric.value}</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Growth Progress */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="font-medium">Growth Progress</span>
|
||||
<span className="text-muted-foreground">{analytics.growthProgress}%</span>
|
||||
</div>
|
||||
<Progress value={analytics.growthProgress} className="h-2" />
|
||||
</div>
|
||||
|
||||
{/* Next Action Card */}
|
||||
<Card className="border-green-100 dark:border-green-700 bg-green-50/50 dark:bg-green-900/50">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="p-2 rounded-lg bg-green-100 dark:bg-green-800">
|
||||
<Timer className="h-4 w-4 text-green-600 dark:text-green-300" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium mb-1">Next Action Required</p>
|
||||
<p className="text-sm text-muted-foreground">{analytics.nextAction}</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Due by {analytics.nextActionDue.toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Map Section */}
|
||||
<Card className="border-green-100 dark:border-green-700">
|
||||
<CardHeader>
|
||||
<CardTitle>Field Map</CardTitle>
|
||||
<CardDescription>View and manage crop location</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="p-0 h-[400px]">
|
||||
<GoogleMapWithDrawing />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Right Column */}
|
||||
<div className="md:col-span-4 space-y-6">
|
||||
{/* Nutrient Levels */}
|
||||
<Card className="border-green-100 dark:border-green-700">
|
||||
<CardHeader>
|
||||
<CardTitle>Nutrient Levels</CardTitle>
|
||||
<CardDescription>Current soil composition</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{[
|
||||
{
|
||||
name: "Nitrogen (N)",
|
||||
value: analytics.nutrientLevels.nitrogen,
|
||||
color: "bg-blue-500 dark:bg-blue-700",
|
||||
},
|
||||
{
|
||||
name: "Phosphorus (P)",
|
||||
value: analytics.nutrientLevels.phosphorus,
|
||||
color: "bg-yellow-500 dark:bg-yellow-700",
|
||||
},
|
||||
{
|
||||
name: "Potassium (K)",
|
||||
value: analytics.nutrientLevels.potassium,
|
||||
color: "bg-green-500 dark:bg-green-700",
|
||||
},
|
||||
].map((nutrient) => (
|
||||
<div key={nutrient.name} className="space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="font-medium">{nutrient.name}</span>
|
||||
<span className="text-muted-foreground">{nutrient.value}%</span>
|
||||
</div>
|
||||
<Progress value={nutrient.value} className={`h-2 ${nutrient.color}`} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Recent Activity */}
|
||||
<Card className="border-green-100 dark:border-green-700">
|
||||
<CardHeader>
|
||||
<CardTitle>Recent Activity</CardTitle>
|
||||
<CardDescription>Latest updates and changes</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ScrollArea className="h-[300px] pr-4">
|
||||
{[...Array(5)].map((_, i) => (
|
||||
<div key={i} className="mb-4 last:mb-0">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="p-2 rounded-lg bg-gray-100 dark:bg-gray-800">
|
||||
<Activity icon={i} />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium">
|
||||
{
|
||||
[
|
||||
"Irrigation completed",
|
||||
"Nutrient levels checked",
|
||||
"Growth measurement taken",
|
||||
"Pest inspection completed",
|
||||
"Soil pH tested",
|
||||
][i]
|
||||
}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">2 hours ago</p>
|
||||
</div>
|
||||
</div>
|
||||
{i < 4 && <Separator className="my-4 dark:bg-slate-700" />}
|
||||
</div>
|
||||
))}
|
||||
</ScrollArea>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Dialogs */}
|
||||
<ChatbotDialog open={isChatOpen} onOpenChange={setIsChatOpen} cropName={crop.name} />
|
||||
<AnalyticsDialog open={isAnalyticsOpen} onOpenChange={setIsAnalyticsOpen} crop={crop} analytics={analytics} />
|
||||
</div>
|
||||
|
||||
<ChatbotDialog open={isChatOpen} onOpenChange={setIsChatOpen} cropName={crop.name} />
|
||||
|
||||
<AnalyticsDialog open={isAnalyticsOpen} onOpenChange={setIsAnalyticsOpen} crop={crop} analytics={analytics} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper component to render an activity icon based on the index.
|
||||
*/
|
||||
function Activity({ icon }: { icon: number }) {
|
||||
const icons = [
|
||||
<Droplets key="0" className="h-4 w-4 text-blue-500 dark:text-blue-300" />,
|
||||
<Leaf key="1" className="h-4 w-4 text-green-500 dark:text-green-300" />,
|
||||
<LineChart key="2" className="h-4 w-4 text-purple-500 dark:text-purple-300" />,
|
||||
<Sprout key="3" className="h-4 w-4 text-yellow-500 dark:text-yellow-300" />,
|
||||
<ThermometerSun key="4" className="h-4 w-4 text-orange-500 dark:text-orange-300" />,
|
||||
];
|
||||
return icons[icon];
|
||||
}
|
||||
|
||||
@ -3,19 +3,32 @@ export interface Crop {
|
||||
farmId: string;
|
||||
name: string;
|
||||
plantedDate: Date;
|
||||
status: "growing" | "harvested" | "planned";
|
||||
expectedHarvest?: Date;
|
||||
status: string;
|
||||
variety?: string;
|
||||
area?: string;
|
||||
healthScore?: number;
|
||||
progress?: number;
|
||||
}
|
||||
|
||||
export interface CropAnalytics {
|
||||
cropId: string;
|
||||
growthProgress: number;
|
||||
humidity: number;
|
||||
temperature: number;
|
||||
sunlight: number;
|
||||
waterLevel: number;
|
||||
growthProgress: number;
|
||||
plantHealth: "good" | "warning" | "critical";
|
||||
nextAction: string;
|
||||
nextActionDue: Date;
|
||||
soilMoisture: number;
|
||||
windSpeed: string;
|
||||
rainfall: string;
|
||||
nutrientLevels: {
|
||||
nitrogen: number;
|
||||
phosphorus: number;
|
||||
potassium: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface Farm {
|
||||
@ -24,6 +37,14 @@ export interface Farm {
|
||||
location: string;
|
||||
type: string;
|
||||
createdAt: Date;
|
||||
area?: string;
|
||||
crops: number;
|
||||
weather?: {
|
||||
temperature: number;
|
||||
humidity: number;
|
||||
rainfall: string;
|
||||
sunlight: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface User {
|
||||
@ -34,5 +55,6 @@ export interface User {
|
||||
Email: string;
|
||||
CreatedAt: string;
|
||||
UpdatedAt: string;
|
||||
Avatar: string;
|
||||
IsActive: boolean;
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user