diff --git a/frontend/api/farm.ts b/frontend/api/farm.ts new file mode 100644 index 0000000..30af22d --- /dev/null +++ b/frontend/api/farm.ts @@ -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 { + // 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 { + 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 { + // 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 }; +} diff --git a/frontend/app/(sidebar)/farms/[farmId]/crops/[cropId]/analytics-dialog.tsx b/frontend/app/(sidebar)/farms/[farmId]/crops/[cropId]/analytics-dialog.tsx index 7beeff7..c7ea5f2 100644 --- a/frontend/app/(sidebar)/farms/[farmId]/crops/[cropId]/analytics-dialog.tsx +++ b/frontend/app/(sidebar)/farms/[farmId]/crops/[cropId]/analytics-dialog.tsx @@ -16,7 +16,7 @@ interface AnalyticsDialogProps { export function AnalyticsDialog({ open, onOpenChange, crop, analytics }: AnalyticsDialogProps) { return ( - + Crop Analytics - {crop.name} @@ -30,30 +30,30 @@ export function AnalyticsDialog({ open, onOpenChange, crop, analytics }: Analyti
- + Growth Rate - +
+2.5%

+20.1% from last week

- + Water Usage - +
15.2L

per day average

- + Sunlight - +
{analytics.sunlight}%
@@ -62,7 +62,7 @@ export function AnalyticsDialog({ open, onOpenChange, crop, analytics }: Analyti
- + Growth Timeline Daily growth rate over time @@ -75,7 +75,7 @@ export function AnalyticsDialog({ open, onOpenChange, crop, analytics }: Analyti
- + Detailed Growth Analysis Comprehensive growth metrics @@ -87,7 +87,7 @@ export function AnalyticsDialog({ open, onOpenChange, crop, analytics }: Analyti - + Environmental Conditions Temperature, humidity, and more diff --git a/frontend/app/(sidebar)/farms/[farmId]/crops/[cropId]/chatbot-dialog.tsx b/frontend/app/(sidebar)/farms/[farmId]/crops/[cropId]/chatbot-dialog.tsx index e704521..72873b0 100644 --- a/frontend/app/(sidebar)/farms/[farmId]/crops/[cropId]/chatbot-dialog.tsx +++ b/frontend/app/(sidebar)/farms/[farmId]/crops/[cropId]/chatbot-dialog.tsx @@ -43,11 +43,11 @@ export function ChatbotDialog({ open, onOpenChange, cropName }: ChatbotDialogPro return ( - + Farming Assistant Chat - +
-
+

Farming Assistant

Ask questions about your {cropName}

@@ -58,7 +58,9 @@ export function ChatbotDialog({ open, onOpenChange, cropName }: ChatbotDialogPro
{message.content}
@@ -67,7 +69,7 @@ export function ChatbotDialog({ open, onOpenChange, cropName }: ChatbotDialogPro
-
+
{ e.preventDefault(); diff --git a/frontend/app/(sidebar)/farms/[farmId]/crops/[cropId]/page.tsx b/frontend/app/(sidebar)/farms/[farmId]/crops/[cropId]/page.tsx index 4ab720b..0b767fd 100644 --- a/frontend/app/(sidebar)/farms/[farmId]/crops/[cropId]/page.tsx +++ b/frontend/app/(sidebar)/farms/[farmId]/crops/[cropId]/page.tsx @@ -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 }) { const router = useRouter(); - const [crop] = useState(getCropById(cropId)); - const analytics = getAnalyticsByCropId(cropId); + const [crop, setCrop] = useState(null); + const [analytics, setAnalytics] = useState(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 ( +
Loading...
+ ); + } - // 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 ( -
- - -
- {/* Left Column - Crop Details */} -
- - -
-
- -
-
-

{crop.name}

-

Planted on {crop.plantedDate.toLocaleDateString()}

-
-
- - {analytics.plantHealth.toUpperCase()} - -
- -
-
-

Growth Progress

- -

{analytics.growthProgress}% Complete

-
- -
-
-
- - Humidity +
+
+ {/* Header */} +
+
+ + + + + + +
+ + + + + + +
+

Growth Timeline

+

Planted on {crop.plantedDate.toLocaleDateString()}

+
+ + + {Math.floor(analytics.growthProgress)}% Complete + +
-

{analytics.humidity}%

-
-
-
- - Temperature -
-

{analytics.temperature}°C

-
-
-
- - Sunlight -
-

{analytics.sunlight}%

-
-
-
- - Water Level -
-

{analytics.waterLevel}%

+
+
+
- - -
-
- - Next Action Required -
-
-

{analytics.nextAction}

-

- Due by {analytics.nextActionDue.toLocaleDateString()} -

-
+
+
+

{crop.name}

+

+ {crop.variety} • {crop.area} +

+
+
+
+
+ + Health Score: {crop.healthScore}% + + + Growing +
-
- - - - - -

Actions

-
- -
- {actions.map((action) => ( - - ))} -
-
-
-
- -
- - - -
- {/*
- -

- Map placeholder -
- Click to view full map + {crop.expectedHarvest ? ( +

+ Expected harvest: {crop.expectedHarvest.toLocaleDateString()}

-
*/} + ) : ( +

Expected harvest date not available

+ )}
-
-
- - - -

Quick Analytics

-
- - - - Growth - Health - Water - - - Growth chart placeholder - - - Health metrics placeholder - - - Water usage placeholder - - - -
+
+
+ + {/* Main Content */} +
+ {/* Left Column */} +
+ {/* Quick Actions */} +
+ {quickActions.map((action) => ( + + ))} +
+ + {/* Environmental Metrics */} + + + Environmental Conditions + Real-time monitoring of growing conditions + + +
+
+ {[ + { + 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) => ( + + +
+
+ +
+
+

{metric.label}

+

{metric.value}

+
+
+
+
+ ))} +
+ + + + {/* Growth Progress */} +
+
+ Growth Progress + {analytics.growthProgress}% +
+ +
+ + {/* Next Action Card */} + + +
+
+ +
+
+

Next Action Required

+

{analytics.nextAction}

+

+ Due by {analytics.nextActionDue.toLocaleDateString()} +

+
+
+
+
+
+
+
+ + {/* Map Section */} + + + Field Map + View and manage crop location + + + + + +
+ + {/* Right Column */} +
+ {/* Nutrient Levels */} + + + Nutrient Levels + Current soil composition + + +
+ {[ + { + 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) => ( +
+
+ {nutrient.name} + {nutrient.value}% +
+ +
+ ))} +
+
+
+ + {/* Recent Activity */} + + + Recent Activity + Latest updates and changes + + + + {[...Array(5)].map((_, i) => ( +
+
+
+ +
+
+

+ { + [ + "Irrigation completed", + "Nutrient levels checked", + "Growth measurement taken", + "Pest inspection completed", + "Soil pH tested", + ][i] + } +

+

2 hours ago

+
+
+ {i < 4 && } +
+ ))} +
+
+
+
+
+ + {/* Dialogs */} + +
- - - -
); } + +/** + * Helper component to render an activity icon based on the index. + */ +function Activity({ icon }: { icon: number }) { + const icons = [ + , + , + , + , + , + ]; + return icons[icon]; +} diff --git a/frontend/types.ts b/frontend/types.ts index c409b3f..e08f7f7 100644 --- a/frontend/types.ts +++ b/frontend/types.ts @@ -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; }