ui: improve crops detailed page

This commit is contained in:
Sosokker 2025-03-07 02:26:24 +07:00
parent 4b772e20d0
commit b1c8b50a86
5 changed files with 553 additions and 206 deletions

171
frontend/api/farm.ts Normal file
View 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 };
}

View File

@ -16,7 +16,7 @@ interface AnalyticsDialogProps {
export function AnalyticsDialog({ open, onOpenChange, crop, analytics }: AnalyticsDialogProps) { export function AnalyticsDialog({ open, onOpenChange, crop, analytics }: AnalyticsDialogProps) {
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[800px]"> <DialogContent className="sm:max-w-[800px] dark:bg-background">
<DialogHeader> <DialogHeader>
<DialogTitle>Crop Analytics - {crop.name}</DialogTitle> <DialogTitle>Crop Analytics - {crop.name}</DialogTitle>
</DialogHeader> </DialogHeader>
@ -30,30 +30,30 @@ export function AnalyticsDialog({ open, onOpenChange, crop, analytics }: Analyti
<TabsContent value="overview" className="space-y-4"> <TabsContent value="overview" className="space-y-4">
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3"> <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"> <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Growth Rate</CardTitle> <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> </CardHeader>
<CardContent> <CardContent>
<div className="text-2xl font-bold">+2.5%</div> <div className="text-2xl font-bold">+2.5%</div>
<p className="text-xs text-muted-foreground">+20.1% from last week</p> <p className="text-xs text-muted-foreground">+20.1% from last week</p>
</CardContent> </CardContent>
</Card> </Card>
<Card> <Card className="dark:bg-slate-800">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Water Usage</CardTitle> <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> </CardHeader>
<CardContent> <CardContent>
<div className="text-2xl font-bold">15.2L</div> <div className="text-2xl font-bold">15.2L</div>
<p className="text-xs text-muted-foreground">per day average</p> <p className="text-xs text-muted-foreground">per day average</p>
</CardContent> </CardContent>
</Card> </Card>
<Card> <Card className="dark:bg-slate-800">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Sunlight</CardTitle> <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> </CardHeader>
<CardContent> <CardContent>
<div className="text-2xl font-bold">{analytics.sunlight}%</div> <div className="text-2xl font-bold">{analytics.sunlight}%</div>
@ -62,7 +62,7 @@ export function AnalyticsDialog({ open, onOpenChange, crop, analytics }: Analyti
</Card> </Card>
</div> </div>
<Card> <Card className="dark:bg-slate-800">
<CardHeader> <CardHeader>
<CardTitle>Growth Timeline</CardTitle> <CardTitle>Growth Timeline</CardTitle>
<CardDescription>Daily growth rate over time</CardDescription> <CardDescription>Daily growth rate over time</CardDescription>
@ -75,7 +75,7 @@ export function AnalyticsDialog({ open, onOpenChange, crop, analytics }: Analyti
</TabsContent> </TabsContent>
<TabsContent value="growth" className="space-y-4"> <TabsContent value="growth" className="space-y-4">
<Card> <Card className="dark:bg-slate-800">
<CardHeader> <CardHeader>
<CardTitle>Detailed Growth Analysis</CardTitle> <CardTitle>Detailed Growth Analysis</CardTitle>
<CardDescription>Comprehensive growth metrics</CardDescription> <CardDescription>Comprehensive growth metrics</CardDescription>
@ -87,7 +87,7 @@ export function AnalyticsDialog({ open, onOpenChange, crop, analytics }: Analyti
</TabsContent> </TabsContent>
<TabsContent value="environment" className="space-y-4"> <TabsContent value="environment" className="space-y-4">
<Card> <Card className="dark:bg-slate-800">
<CardHeader> <CardHeader>
<CardTitle>Environmental Conditions</CardTitle> <CardTitle>Environmental Conditions</CardTitle>
<CardDescription>Temperature, humidity, and more</CardDescription> <CardDescription>Temperature, humidity, and more</CardDescription>

View File

@ -43,11 +43,11 @@ export function ChatbotDialog({ open, onOpenChange, cropName }: ChatbotDialogPro
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={onOpenChange}>
<VisuallyHidden> <VisuallyHidden>
<DialogTitle></DialogTitle> <DialogTitle>Farming Assistant Chat</DialogTitle>
</VisuallyHidden> </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="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> <h2 className="text-lg font-semibold">Farming Assistant</h2>
<p className="text-sm text-muted-foreground">Ask questions about your {cropName}</p> <p className="text-sm text-muted-foreground">Ask questions about your {cropName}</p>
</div> </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 key={i} className={`flex ${message.role === "user" ? "justify-end" : "justify-start"}`}>
<div <div
className={`rounded-lg px-4 py-2 max-w-[80%] ${ 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} {message.content}
</div> </div>
@ -67,7 +69,7 @@ export function ChatbotDialog({ open, onOpenChange, cropName }: ChatbotDialogPro
</div> </div>
</ScrollArea> </ScrollArea>
<div className="p-4 border-t"> <div className="p-4 border-t dark:border-slate-700">
<form <form
onSubmit={(e) => { onSubmit={(e) => {
e.preventDefault(); e.preventDefault();

View File

@ -1,255 +1,407 @@
"use client"; "use client";
import React, { useState } from "react"; import React, { useEffect, useState } from "react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { import {
ArrowLeft, ArrowLeft,
MapPin,
Sprout, Sprout,
LineChart, LineChart,
MessageSquare, MessageSquare,
Settings, Settings,
AlertCircle,
Droplets, Droplets,
Sun, Sun,
ThermometerSun, ThermometerSun,
Timer, Timer,
ListCollapse, ListCollapse,
Calendar,
Leaf,
CloudRain,
Wind,
} from "lucide-react"; } 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 { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import { Progress } from "@/components/ui/progress"; import { Progress } from "@/components/ui/progress";
import { Badge } from "@/components/ui/badge"; 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 { ChatbotDialog } from "./chatbot-dialog";
import { AnalyticsDialog } from "./analytics-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 type { Crop, CropAnalytics } from "@/types";
import GoogleMapWithDrawing from "@/components/google-map-with-drawing"; import GoogleMapWithDrawing from "@/components/google-map-with-drawing";
import { fetchCropById, fetchAnalyticsByCropId } from "@/api/farm";
const getCropById = (id: string): Crop => { interface CropDetailPageParams {
return { farmId: string;
id, cropId: string;
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);
export default function CropDetailPage({ params }: { params: Promise<CropDetailPageParams> }) {
const router = useRouter(); const router = useRouter();
const [crop] = useState(getCropById(cropId)); const [crop, setCrop] = useState<Crop | null>(null);
const analytics = getAnalyticsByCropId(cropId); 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 = { const healthColors = {
good: "text-green-500", good: "text-green-500 bg-green-50 dark:bg-green-900",
warning: "text-yellow-500", warning: "text-yellow-500 bg-yellow-50 dark:bg-yellow-900",
critical: "text-red-500", critical: "text-red-500 bg-red-50 dark:bg-red-900",
}; };
const actions = [ const quickActions = [
{ {
title: "Analytics", title: "Analytics",
icon: LineChart, icon: LineChart,
description: "View detailed growth analytics", description: "View detailed growth analytics",
onClick: () => setIsAnalyticsOpen(true), onClick: () => setIsAnalyticsOpen(true),
color: "bg-blue-50 dark:bg-blue-900 text-blue-600 dark:text-blue-300",
}, },
{ {
title: "Chat Assistant", title: "Chat Assistant",
icon: MessageSquare, icon: MessageSquare,
description: "Get help and advice", description: "Get help and advice",
onClick: () => setIsChatOpen(true), 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, icon: ListCollapse,
description: "View detailed of crop", description: "View detailed information",
onClick: () => console.log("Detailed clicked"), onClick: () => console.log("Details clicked"),
color: "bg-purple-50 dark:bg-purple-900 text-purple-600 dark:text-purple-300",
}, },
{ {
title: "Settings", title: "Settings",
icon: Settings, icon: Settings,
description: "Configure crop settings", description: "Configure crop settings",
onClick: () => console.log("Settings clicked"), onClick: () => console.log("Settings clicked"),
}, color: "bg-gray-50 dark:bg-gray-900 text-gray-600 dark:text-gray-300",
{
title: "Report Issue",
icon: AlertCircle,
description: "Report a problem",
onClick: () => console.log("Report clicked"),
}, },
]; ];
const [isChatOpen, setIsChatOpen] = useState(false);
const [isAnalyticsOpen, setIsAnalyticsOpen] = useState(false);
return ( return (
<div className="container max-w-screen-xl p-8"> <div className="min-h-screen bg-background text-foreground">
<Button variant="ghost" className="mb-4" onClick={() => router.back()}> <div className="container max-w-7xl p-6 mx-auto">
<ArrowLeft className="mr-2 h-4 w-4" /> Back to Farm {/* 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> </Button>
<HoverCard>
<div className="grid gap-6 md:grid-cols-2"> <HoverCardTrigger asChild>
{/* Left Column - Crop Details */} <Button variant="outline" className="gap-2">
<div className="space-y-6"> <Calendar className="h-4 w-4" /> Timeline
<Card> </Button>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> </HoverCardTrigger>
<div className="flex items-center space-x-2"> <HoverCardContent className="w-80">
<div className="h-8 w-8 rounded-full bg-primary/10 flex items-center justify-center"> <div className="flex justify-between space-x-4">
<Sprout className="h-4 w-4 text-primary" /> <Avatar>
</div> <AvatarImage src="/placeholder.svg" />
<div> <AvatarFallback>
<h1 className="text-2xl font-bold">{crop.name}</h1> <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> <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> </div>
</div> </div>
<Badge variant="outline" className={healthColors[analytics.plantHealth]}> </div>
{analytics.plantHealth.toUpperCase()} </HoverCardContent>
</HoverCard>
</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>
<Badge variant="outline" className="bg-blue-50 dark:bg-blue-900 text-blue-600 dark:text-blue-300">
Growing
</Badge>
</div>
{crop.expectedHarvest ? (
<p className="text-sm text-muted-foreground mt-1">
Expected harvest: {crop.expectedHarvest.toLocaleDateString()}
</p>
) : (
<p className="text-sm text-muted-foreground mt-1">Expected harvest date not available</p>
)}
</div>
</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> </CardHeader>
<CardContent> <CardContent>
<div className="space-y-4"> <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> <div>
<p className="text-sm text-muted-foreground mb-2">Growth Progress</p> <p className="text-sm font-medium text-muted-foreground">{metric.label}</p>
<Progress value={analytics.growthProgress} className="h-2" /> <p className="text-2xl font-semibold tracking-tight">{metric.value}</p>
<p className="text-sm text-muted-foreground mt-1">{analytics.growthProgress}% Complete</p>
</div> </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>
<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>
</CardContent>
</Card>
))}
</div> </div>
<Separator /> <Separator />
{/* Growth Progress */}
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center gap-2"> <div className="flex justify-between text-sm">
<Timer className="h-4 w-4 text-primary" /> <span className="font-medium">Growth Progress</span>
<span className="text-sm font-medium">Next Action Required</span> <span className="text-muted-foreground">{analytics.growthProgress}%</span>
</div> </div>
<div className="bg-muted/50 p-4 rounded-lg"> <Progress value={analytics.growthProgress} className="h-2" />
<p className="font-medium">{analytics.nextAction}</p> </div>
<p className="text-sm text-muted-foreground">
{/* 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()} Due by {analytics.nextActionDue.toLocaleDateString()}
</p> </p>
</div> </div>
</div> </div>
</CardContent>
</Card>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
<Card> {/* Map Section */}
<Card className="border-green-100 dark:border-green-700">
<CardHeader> <CardHeader>
<h2 className="text-lg font-semibold">Actions</h2> <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> </CardHeader>
<CardContent> <CardContent>
<div className="grid grid-cols-2 gap-4"> <div className="space-y-4">
{actions.map((action) => ( {[
<Button {
key={action.title} name: "Nitrogen (N)",
variant="outline" value: analytics.nutrientLevels.nitrogen,
className="h-auto p-4 flex flex-col items-center gap-2" color: "bg-blue-500 dark:bg-blue-700",
onClick={action.onClick}> },
<action.icon className="h-6 w-6" /> {
<span className="font-medium">{action.title}</span> name: "Phosphorus (P)",
<span className="text-xs text-muted-foreground text-center">{action.description}</span> value: analytics.nutrientLevels.phosphorus,
</Button> 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> </div>
</CardContent> </CardContent>
</Card> </Card>
</div>
<div className="space-y-6"> {/* Recent Activity */}
<Card className="h-[400px] mb-32"> <Card className="border-green-100 dark:border-green-700">
<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
</p>
</div> */}
</div>
</CardContent>
</Card>
<Card>
<CardHeader> <CardHeader>
<h2 className="text-lg font-semibold">Quick Analytics</h2> <CardTitle>Recent Activity</CardTitle>
<CardDescription>Latest updates and changes</CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<Tabs defaultValue="growth"> <ScrollArea className="h-[300px] pr-4">
<TabsList className="grid w-full grid-cols-3"> {[...Array(5)].map((_, i) => (
<TabsTrigger value="growth">Growth</TabsTrigger> <div key={i} className="mb-4 last:mb-0">
<TabsTrigger value="health">Health</TabsTrigger> <div className="flex items-start gap-4">
<TabsTrigger value="water">Water</TabsTrigger> <div className="p-2 rounded-lg bg-gray-100 dark:bg-gray-800">
</TabsList> <Activity icon={i} />
<TabsContent value="growth" className="flex items-center justify-center text-muted-foreground"> </div>
Growth chart placeholder <div>
</TabsContent> <p className="text-sm font-medium">
<TabsContent value="health" className="flex items-center justify-center text-muted-foreground"> {
Health metrics placeholder [
</TabsContent> "Irrigation completed",
<TabsContent value="water" className="flex items-center justify-center text-muted-foreground"> "Nutrient levels checked",
Water usage placeholder "Growth measurement taken",
</TabsContent> "Pest inspection completed",
</Tabs> "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> </CardContent>
</Card> </Card>
</div> </div>
</div> </div>
{/* Dialogs */}
<ChatbotDialog open={isChatOpen} onOpenChange={setIsChatOpen} cropName={crop.name} /> <ChatbotDialog open={isChatOpen} onOpenChange={setIsChatOpen} cropName={crop.name} />
<AnalyticsDialog open={isAnalyticsOpen} onOpenChange={setIsAnalyticsOpen} crop={crop} analytics={analytics} /> <AnalyticsDialog open={isAnalyticsOpen} onOpenChange={setIsAnalyticsOpen} crop={crop} analytics={analytics} />
</div> </div>
</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];
}

View File

@ -3,19 +3,32 @@ export interface Crop {
farmId: string; farmId: string;
name: string; name: string;
plantedDate: Date; plantedDate: Date;
status: "growing" | "harvested" | "planned"; expectedHarvest?: Date;
status: string;
variety?: string;
area?: string;
healthScore?: number;
progress?: number;
} }
export interface CropAnalytics { export interface CropAnalytics {
cropId: string; cropId: string;
growthProgress: number;
humidity: number; humidity: number;
temperature: number; temperature: number;
sunlight: number; sunlight: number;
waterLevel: number; waterLevel: number;
growthProgress: number;
plantHealth: "good" | "warning" | "critical"; plantHealth: "good" | "warning" | "critical";
nextAction: string; nextAction: string;
nextActionDue: Date; nextActionDue: Date;
soilMoisture: number;
windSpeed: string;
rainfall: string;
nutrientLevels: {
nitrogen: number;
phosphorus: number;
potassium: number;
};
} }
export interface Farm { export interface Farm {
@ -24,6 +37,14 @@ export interface Farm {
location: string; location: string;
type: string; type: string;
createdAt: Date; createdAt: Date;
area?: string;
crops: number;
weather?: {
temperature: number;
humidity: number;
rainfall: string;
sunlight: number;
};
} }
export interface User { export interface User {
@ -34,5 +55,6 @@ export interface User {
Email: string; Email: string;
CreatedAt: string; CreatedAt: string;
UpdatedAt: string; UpdatedAt: string;
Avatar: string;
IsActive: boolean; IsActive: boolean;
} }