diff --git a/frontend/app/(sidebar)/farms/[farmId]/add-crop-form.tsx b/frontend/app/(sidebar)/farms/[farmId]/add-crop-form.tsx index ce33e21..c18b374 100644 --- a/frontend/app/(sidebar)/farms/[farmId]/add-crop-form.tsx +++ b/frontend/app/(sidebar)/farms/[farmId]/add-crop-form.tsx @@ -7,62 +7,131 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; -import { Crop } from "@/types"; -import { cropFormSchema } from "@/schemas/form.schema"; +import { useQuery } from "@tanstack/react-query"; +import { getPlants, PlantResponse } from "@/api/plant"; +import { Loader2 } from "lucide-react"; +import { Cropland } from "@/types"; + +// Update schema to reflect Cropland fields needed for creation +// Removed plantedDate as it's derived from createdAt on backend +// Added plantId, landSize, growthStage, priority +const cropFormSchema = z.object({ + name: z.string().min(2, "Crop name must be at least 2 characters"), + plantId: z.string().uuid("Please select a valid plant"), // Changed from name to ID + status: z.enum(["planned", "growing", "harvested", "fallow"]), // Added fallow + landSize: z.preprocess( + (val) => parseFloat(z.string().parse(val)), // Convert string input to number + z.number().positive("Land size must be a positive number") + ), + growthStage: z.string().min(1, "Growth stage is required"), + priority: z.preprocess( + (val) => parseInt(z.string().parse(val), 10), // Convert string input to number + z.number().int().min(0, "Priority must be non-negative") + ), + // GeoFeature will be handled separately by the map component later +}); interface AddCropFormProps { - onSubmit: (data: Partial) => Promise; + onSubmit: (data: Partial) => Promise; // Expect Partial onCancel: () => void; + isSubmitting: boolean; // Receive submitting state } -export function AddCropForm({ onSubmit, onCancel }: AddCropFormProps) { +export function AddCropForm({ onSubmit, onCancel, isSubmitting }: AddCropFormProps) { + // Fetch plants for the dropdown + const { + data: plantData, + isLoading: isLoadingPlants, + isError: isErrorPlants, + } = useQuery({ + queryKey: ["plants"], + queryFn: getPlants, + staleTime: 1000 * 60 * 60, // Cache for 1 hour + }); + const form = useForm>({ resolver: zodResolver(cropFormSchema), defaultValues: { name: "", - plantedDate: "", + plantId: "", // Initialize plantId status: "planned", + landSize: 0, + growthStage: "Planned", + priority: 1, }, }); const handleSubmit = (values: z.infer) => { + // Submit data shaped like Partial onSubmit({ - ...values, - plantedDate: new Date(values.plantedDate), + name: values.name, + plantId: values.plantId, + status: values.status, + landSize: values.landSize, + growthStage: values.growthStage, + priority: values.priority, + // farmId is added in the parent component's mutationFn + // geoFeature would be passed separately if using map here }); }; return (
- + ( - Crop Name + Cropland Name - + )} /> + {/* Plant Selection */} ( - Planted Date - - - + Select Plant + )} /> + {/* Status Selection */} Planned Growing Harvested + Fallow @@ -86,11 +156,73 @@ export function AddCropForm({ onSubmit, onCancel }: AddCropFormProps) { )} /> -
- - +
diff --git a/frontend/app/(sidebar)/farms/[farmId]/crop-card.tsx b/frontend/app/(sidebar)/farms/[farmId]/crop-card.tsx index e4e0d35..4fb6d95 100644 --- a/frontend/app/(sidebar)/farms/[farmId]/crop-card.tsx +++ b/frontend/app/(sidebar)/farms/[farmId]/crop-card.tsx @@ -1,19 +1,30 @@ "use client"; import { Card, CardContent, CardHeader, CardFooter } from "@/components/ui/card"; -import { Sprout, Calendar, ArrowRight, BarChart } from "lucide-react"; +import { Calendar, ArrowRight, Layers } from "lucide-react"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; -import { Progress } from "@/components/ui/progress"; -import type { Crop } from "@/types"; +import type { Cropland } from "@/types"; +// =================================================================== +// Component Props: CropCard expects a cropland object and an optional click handler. +// =================================================================== interface CropCardProps { - crop: Crop; + crop: Cropland; // Crop data conforming to the Cropland type onClick?: () => void; } +// =================================================================== +// Component: CropCard +// - Displays summary information about a crop, including status, +// created date, and growth stage using an expressive card UI. +// =================================================================== export function CropCard({ crop, onClick }: CropCardProps) { - const statusColors = { + // --------------------------------------------------------------- + // Status color mapping: Determines badge styling based on crop status. + // Default colors provided for unknown statuses. + // --------------------------------------------------------------- + const statusColors: Record = { growing: { bg: "bg-green-50 dark:bg-green-900", text: "text-green-600 dark:text-green-300", @@ -29,67 +40,73 @@ export function CropCard({ crop, onClick }: CropCardProps) { text: "text-blue-600 dark:text-blue-300", border: "border-blue-200", }, + fallow: { + bg: "bg-gray-50 dark:bg-gray-900", + text: "text-gray-600 dark:text-gray-400", + border: "border-gray-200", + }, + default: { + bg: "bg-gray-100 dark:bg-gray-700", + text: "text-gray-800 dark:text-gray-200", + border: "border-gray-300", + }, }; - const statusColor = statusColors[crop.status as keyof typeof statusColors]; + // --------------------------------------------------------------- + // Derive styling based on crop status (ignoring case). + // --------------------------------------------------------------- + const statusKey = crop.status?.toLowerCase() || "default"; // Use camelCase status + const statusColor = statusColors[statusKey] || statusColors.default; + // --------------------------------------------------------------- + // Format metadata for display: creation date and area. + // --------------------------------------------------------------- + const displayDate = crop.createdAt ? new Date(crop.createdAt).toLocaleDateString() : "N/A"; // Use camelCase createdAt + const displayArea = typeof crop.landSize === "number" ? `${crop.landSize} ha` : "N/A"; // Use camelCase landSize + + // =================================================================== + // Render: Crop information card with clickable behavior. + // =================================================================== return ( + className={`w-full h-full flex flex-col overflow-hidden transition-all duration-200 hover:shadow-lg border-muted/60 bg-card dark:bg-card hover:bg-muted/10 dark:hover:bg-slate-700 cursor-pointer`}>
- {crop.status} + {crop.status || "Unknown"}
- {crop.plantedDate.toLocaleDateString()} + {displayDate}
- +
-
- -
+ {/* ... icon ... */}
-

{crop.name}

+

{crop.name}

{/* Use camelCase name */}

- {crop.variety} • {crop.area} + {crop.growthStage || "N/A"} • {displayArea} {/* Use camelCase growthStage */}

- - {crop.status !== "planned" && ( -
-
- Progress - {crop.progress}% -
- -
- )} - - {crop.status === "growing" && ( + {crop.growthStage && (
-
- - Health: {crop.healthScore}% +
+ + {/* Use camelCase growthStage */} + Stage: {crop.growthStage}
)}
- + diff --git a/frontend/app/(sidebar)/farms/[farmId]/crop-dialog.tsx b/frontend/app/(sidebar)/farms/[farmId]/crop-dialog.tsx index c57f3cd..bf1d5b6 100644 --- a/frontend/app/(sidebar)/farms/[farmId]/crop-dialog.tsx +++ b/frontend/app/(sidebar)/farms/[farmId]/crop-dialog.tsx @@ -1,125 +1,301 @@ +// crop-dialog.tsx "use client"; -import { useState } from "react"; -import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog"; +import React, { useState, useMemo, useCallback, useEffect } from "react"; +import { useQuery } from "@tanstack/react-query"; +import { useMapsLibrary } from "@vis.gl/react-google-maps"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, + DialogDescription, +} from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; -import { Card } from "@/components/ui/card"; -import { Check, MapPin } from "lucide-react"; +import { Card, CardContent } from "@/components/ui/card"; +import { + Check, + Sprout, + AlertTriangle, + Loader2, + CalendarDays, + Thermometer, + Droplets, + MapPin, + Maximize, +} from "lucide-react"; import { cn } from "@/lib/utils"; -import type { Crop } from "@/types"; -import { VisuallyHidden } from "@radix-ui/react-visually-hidden"; -import GoogleMapWithDrawing from "@/components/google-map-with-drawing"; - -interface Plant { - id: string; - name: string; - image: string; - growthTime: string; -} - -const plants: Plant[] = [ - { - id: "durian", - name: "Durian", - image: "/placeholder.svg?height=80&width=80", - growthTime: "4-5 months", - }, - { - id: "mango", - name: "Mango", - image: "/placeholder.svg?height=80&width=80", - growthTime: "3-4 months", - }, - { - id: "coconut", - name: "Coconut", - image: "/placeholder.svg?height=80&width=80", - growthTime: "5-6 months", - }, -]; +// Import the updated/new types +import type { Cropland, GeoFeatureData, GeoPosition } from "@/types"; +import { PlantResponse } from "@/api/plant"; +import { getPlants } from "@/api/plant"; +// Import the map component and the ShapeData type (ensure ShapeData in types.ts matches this) +import GoogleMapWithDrawing, { type ShapeData } from "@/components/google-map-with-drawing"; interface CropDialogProps { open: boolean; onOpenChange: (open: boolean) => void; - onSubmit: (data: Partial) => Promise; + onSubmit: (data: Partial) => Promise; + isSubmitting: boolean; } -export function CropDialog({ open, onOpenChange, onSubmit }: CropDialogProps) { - const [selectedPlant, setSelectedPlant] = useState(null); - const [location, setLocation] = useState({ lat: 13.7563, lng: 100.5018 }); // Bangkok coordinates +export function CropDialog({ open, onOpenChange, onSubmit, isSubmitting }: CropDialogProps) { + // --- State --- + const [selectedPlantUUID, setSelectedPlantUUID] = useState(null); + // State to hold the structured GeoFeature data + const [geoFeature, setGeoFeature] = useState(null); + const [calculatedArea, setCalculatedArea] = useState(null); // Keep for display + // --- Load Google Maps Geometry Library --- + const geometryLib = useMapsLibrary("geometry"); + + // --- Fetch Plants --- + const { + data: plantData, + isLoading: isLoadingPlants, + isError: isErrorPlants, + error: errorPlants, + } = useQuery({ + queryKey: ["plants"], + queryFn: getPlants, + staleTime: 1000 * 60 * 60, + refetchOnWindowFocus: false, + }); + const plants = useMemo(() => plantData?.plants || [], [plantData]); + const selectedPlant = useMemo(() => { + return plants.find((p) => p.uuid === selectedPlantUUID); + }, [plants, selectedPlantUUID]); + + // --- Reset State on Dialog Close --- + useEffect(() => { + if (!open) { + setSelectedPlantUUID(null); + setGeoFeature(null); // Reset geoFeature state + setCalculatedArea(null); + } + }, [open]); + + // --- Map Interaction Handler --- + const handleShapeDrawn = useCallback( + (data: ShapeData) => { + console.log("Shape drawn:", data); + if (!geometryLib) { + console.warn("Geometry library not loaded yet."); + return; + } + + let feature: GeoFeatureData | null = null; + let area: number | null = null; + + // Helper to ensure path points are valid GeoPositions + const mapPath = (path?: { lat: number; lng: number }[]): GeoPosition[] => + (path || []).map((p) => ({ lat: p.lat, lng: p.lng })); + + // Helper to ensure position is a valid GeoPosition + const mapPosition = (pos?: { lat: number; lng: number }): GeoPosition | null => + pos ? { lat: pos.lat, lng: pos.lng } : null; + + if (data.type === "polygon" && data.path && data.path.length > 0) { + const geoPath = mapPath(data.path); + feature = { type: "polygon", path: geoPath }; + // Use original path for calculation if library expects {lat, lng} + area = geometryLib.spherical.computeArea(data.path); + console.log("Polygon drawn, Area:", area, "m²"); + } else if (data.type === "polyline" && data.path && data.path.length > 0) { + const geoPath = mapPath(data.path); + feature = { type: "polyline", path: geoPath }; + area = null; + console.log("Polyline drawn, Path:", data.path); + } else if (data.type === "marker" && data.position) { + const geoPos = mapPosition(data.position); + if (geoPos) { + feature = { type: "marker", position: geoPos }; + } + area = null; + console.log("Marker drawn at:", data.position); + } else { + console.log(`Ignoring shape type: ${data.type} or empty path/position`); + feature = null; + area = null; + } + + setGeoFeature(feature); + setCalculatedArea(area); + }, + [geometryLib] // Depend on geometryLib + ); + + // --- Submit Handler --- const handleSubmit = async () => { - if (!selectedPlant) return; + // Check for geoFeature instead of just drawnPath + if (!selectedPlantUUID || !geoFeature) { + alert("Please select a plant and define a feature (marker, polygon, or polyline) on the map."); + return; + } + // selectedPlant is derived from state using useMemo + if (!selectedPlant) { + alert("Selected plant not found."); // Should not happen if UUID is set + return; + } - await onSubmit({ - name: plants.find((p) => p.id === selectedPlant)?.name || "", - plantedDate: new Date(), - status: "planned", - }); + const cropData: Partial = { + // Default name, consider making this editable + name: `${selectedPlant.name} Field ${Math.floor(100 + Math.random() * 900)}`, + plantId: selectedPlant.uuid, + status: "planned", // Default status + // Use calculatedArea if available (only for polygons), otherwise maybe 0 + // The backend might ignore this if it calculates based on GeoFeature + landSize: calculatedArea ?? 0, + growthStage: "Planned", // Default growth stage + priority: 1, // Default priority + geoFeature: geoFeature, // Add the structured geoFeature data + // FarmID will be added in the page component mutationFn + }; - setSelectedPlant(null); - onOpenChange(false); + console.log("Submitting Cropland Data:", cropData); + + try { + await onSubmit(cropData); + // State reset handled by useEffect watching 'open' + } catch (error) { + console.error("Submission failed in dialog:", error); + // Optionally show an error message to the user within the dialog + } }; + // --- Render --- return ( - - - - -
- {/* Left side - Plant Selection */} -
-

Select Plant to Grow

-
- {plants.map((plant) => ( - setSelectedPlant(plant.id)}> -
- {plant.name} -
-
-

{plant.name}

- {selectedPlant === plant.id && } -
-

Growth time: {plant.growthTime}

-
-
-
- ))} -
-
+ + + Create New Cropland + + Select a plant and draw the cropland boundary or mark its location on the map. + + - {/* Right side - Map */} -
-
-
- +
+ {/* Left Side: Plant Selection */} +
+

1. Select Plant

+ {/* Plant selection UI */} + {isLoadingPlants && ( +
+ + Loading plants...
-
+ )} + {isErrorPlants && ( +
+ + Error loading plants: {(errorPlants as Error)?.message} +
+ )} + {!isLoadingPlants && !isErrorPlants && plants.length === 0 && ( +
No plants available.
+ )} + {!isLoadingPlants && !isErrorPlants && plants.length > 0 && ( +
+ {plants.map((plant) => ( + setSelectedPlantUUID(plant.uuid)}> + +
+
+ +
+
+
+

+ {plant.name} ({plant.variety}) +

+ {selectedPlantUUID === plant.uuid && ( + + )} +
+
+

+ Maturity: ~{plant.daysToMaturity ?? "N/A"} days +

+

+ Temp: {plant.optimalTemp ?? "N/A"}°C +

+

+ Water: {plant.waterNeeds ?? "N/A"} +

+
+
+
+
+
+ ))} +
+ )}
- {/* Footer */} -
-
- - + {/* Right Side: Map */} +
+

2. Define Boundary / Location

+
+ + + {/* Display feedback based on drawn shape */} + {geoFeature?.type === "polygon" && calculatedArea !== null && ( +
+ + Area: {calculatedArea.toFixed(2)} m² +
+ )} + {geoFeature?.type === "polyline" && geoFeature.path && ( +
+ + Boundary path defined ({geoFeature.path.length} points). +
+ )} + {geoFeature?.type === "marker" && geoFeature.position && ( +
+ + Marker set at {geoFeature.position.lat.toFixed(4)}, {geoFeature.position.lng.toFixed(4)}. +
+ )} + {!geometryLib && ( +
+ Loading map tools... +
+ )}
+

+ Use the drawing tools (Polygon , Polyline{" "} + , Marker ) above + the map. Area is calculated for polygons. +

+ + {/* Dialog Footer */} + + + {/* Disable submit if no plant OR no feature is selected */} + +
); 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 c7ea5f2..2ba8a75 100644 --- a/frontend/app/(sidebar)/farms/[farmId]/crops/[cropId]/analytics-dialog.tsx +++ b/frontend/app/(sidebar)/farms/[farmId]/crops/[cropId]/analytics-dialog.tsx @@ -4,12 +4,12 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/u import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { LineChart, Sprout, Droplets, Sun } from "lucide-react"; -import type { Crop, CropAnalytics } from "@/types"; +import type { CropAnalytics, Cropland } from "@/types"; interface AnalyticsDialogProps { open: boolean; onOpenChange: (open: boolean) => void; - crop: Crop; + crop: Cropland; analytics: CropAnalytics; } diff --git a/frontend/app/(sidebar)/farms/[farmId]/crops/[cropId]/page.tsx b/frontend/app/(sidebar)/farms/[farmId]/crops/[cropId]/page.tsx index 4c444ab..461928b 100644 --- a/frontend/app/(sidebar)/farms/[farmId]/crops/[cropId]/page.tsx +++ b/frontend/app/(sidebar)/farms/[farmId]/crops/[cropId]/page.tsx @@ -1,30 +1,29 @@ "use client"; -import React, { useEffect, useState } from "react"; -import { useRouter } from "next/navigation"; +import React, { useMemo, useState } from "react"; +import { useRouter, useParams } from "next/navigation"; +import { useQuery } from "@tanstack/react-query"; import { ArrowLeft, - Sprout, LineChart, - MessageSquare, Settings, Droplets, Sun, ThermometerSun, Timer, ListCollapse, - Calendar, Leaf, CloudRain, Wind, + Home, + ChevronRight, + AlertTriangle, + Loader2, + LeafIcon, + History, + Bot, } from "lucide-react"; -import { - Card, - CardContent, - CardHeader, - CardTitle, - CardDescription, -} 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"; @@ -32,56 +31,121 @@ import { Badge } from "@/components/ui/badge"; 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 { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import type { Cropland, CropAnalytics, Farm } from "@/types"; +import { getFarm } from "@/api/farm"; +import { getPlants, PlantResponse } from "@/api/plant"; +import { getCropById, fetchCropAnalytics } from "@/api/crop"; import GoogleMapWithDrawing from "@/components/google-map-with-drawing"; -import { fetchCropById, fetchAnalyticsByCropId } from "@/api/farm"; -interface CropDetailPageParams { - farmId: string; - cropId: string; -} - -export default function CropDetailPage({ - params, -}: { - params: Promise; -}) { +export default function CropDetailPage() { const router = useRouter(); - const [crop, setCrop] = useState(null); - const [analytics, setAnalytics] = useState(null); + const params = useParams<{ farmId: string; cropId: string }>(); + const { farmId, cropId } = params; + 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]); + // --- Fetch Farm Data --- + const { data: farm, isLoading: isLoadingFarm } = useQuery({ + queryKey: ["farm", farmId], + queryFn: () => getFarm(farmId), + enabled: !!farmId, + staleTime: 5 * 60 * 1000, + }); - if (!crop || !analytics) { + // --- Fetch Cropland Data --- + const { + data: cropland, + isLoading: isLoadingCropland, + isError: isErrorCropland, + error: errorCropland, + } = useQuery({ + queryKey: ["crop", cropId], + queryFn: () => getCropById(cropId), + enabled: !!cropId, + staleTime: 60 * 1000, + }); + + // --- Fetch All Plants Data --- + const { + data: plantData, + isLoading: isLoadingPlants, + isError: isErrorPlants, + error: errorPlants, + } = useQuery({ + queryKey: ["plants"], + queryFn: getPlants, + staleTime: 1000 * 60 * 60, + refetchOnWindowFocus: false, + }); + + // --- Derive specific Plant --- + const plant = useMemo(() => { + if (!cropland?.plantId || !plantData?.plants) return null; + return plantData.plants.find((p) => p.uuid === cropland.plantId); + }, [cropland, plantData]); + + // --- Fetch Crop Analytics Data --- + const { + data: analytics, // Type is CropAnalytics | null + isLoading: isLoadingAnalytics, + isError: isErrorAnalytics, + error: errorAnalytics, + } = useQuery({ + queryKey: ["cropAnalytics", cropId], + queryFn: () => fetchCropAnalytics(cropId), + enabled: !!cropId, + staleTime: 5 * 60 * 1000, + }); + + // --- Combined Loading and Error States --- + const isLoading = isLoadingFarm || isLoadingCropland || isLoadingPlants || isLoadingAnalytics; + const isError = isErrorCropland || isErrorPlants || isErrorAnalytics; // Prioritize crop/analytics errors + const error = errorCropland || errorPlants || errorAnalytics; + + // --- Loading State --- + if (isLoading) { return (
- Loading... + + Loading crop details...
); } + // --- Error State --- + if (isError || !cropland) { + console.error("Error loading crop details:", error); + return ( +
+ + + + Error Loading Crop Details + + {isErrorCropland + ? `Crop with ID ${cropId} not found or could not be loaded.` + : (error as Error)?.message || "An unexpected error occurred."} + + +
+ ); + } + + // --- Data available, render page --- const healthColors = { - 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", + good: "text-green-500 bg-green-50 dark:bg-green-900 border-green-200", + warning: "text-yellow-500 bg-yellow-50 dark:bg-yellow-900 border-yellow-200", + critical: "text-red-500 bg-red-50 dark:bg-red-900 border-red-200", }; + const healthStatus = analytics?.plantHealth || "good"; const quickActions = [ { @@ -93,7 +157,7 @@ export default function CropDetailPage({ }, { title: "Chat Assistant", - icon: MessageSquare, + icon: Bot, description: "Get help and advice", onClick: () => setIsChatOpen(true), color: "bg-green-50 dark:bg-green-900 text-green-600 dark:text-green-300", @@ -102,96 +166,89 @@ export default function CropDetailPage({ title: "Crop Details", icon: ListCollapse, description: "View detailed information", - onClick: () => console.log("Details clicked"), - color: - "bg-purple-50 dark:bg-purple-900 text-purple-600 dark:text-purple-300", + onClick: () => console.log("Details clicked - Placeholder"), + 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"), + onClick: () => console.log("Settings clicked - Placeholder"), color: "bg-gray-50 dark:bg-gray-900 text-gray-600 dark:text-gray-300", }, ]; + const plantedDate = cropland.createdAt ? new Date(cropland.createdAt) : null; + const daysToMaturity = plant?.daysToMaturity; // Use camelCase + const expectedHarvestDate = + plantedDate && daysToMaturity ? new Date(plantedDate.getTime() + daysToMaturity * 24 * 60 * 60 * 1000) : null; + + const growthProgress = analytics?.growthProgress ?? 0; // Get from analytics + const displayArea = typeof cropland.landSize === "number" ? `${cropland.landSize} ha` : "N/A"; // Use camelCase + return (
+ {/* Breadcrumbs */} + + {/* Header */}
- - - - - -
- - - - - - -
-

Growth Timeline

-

- Planted on {crop.plantedDate.toLocaleDateString()} -

-
- - - {Math.floor(analytics.growthProgress)}% Complete - - -
-
-
-
-
+ {/* Hover Card (removed for simplicity, add back if needed) */}
-

{crop.name}

+

{cropland.name}

{/* Use camelCase */}

- {crop.variety} • {crop.area} + {plant?.variety || "Unknown Variety"} • {displayArea} {/* Use camelCase */}

- - Health Score: {crop.healthScore}% - - - Growing + + {cropland.status} {/* Use camelCase */}
- {crop.expectedHarvest ? ( + {expectedHarvestDate ? (

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

) : ( -

- Expected harvest date not available -

+

Expected harvest date not available

)}
@@ -208,138 +265,123 @@ export default function CropDetailPage({ ))}
{/* Environmental Metrics */} - + Environmental Conditions - - Real-time monitoring of growing conditions - + Real-time monitoring data
-
+
{[ { icon: ThermometerSun, label: "Temperature", - value: `${analytics.temperature}°C`, + value: analytics?.temperature ? `${analytics.temperature}°C` : "N/A", color: "text-orange-500 dark:text-orange-300", bg: "bg-orange-50 dark:bg-orange-900", }, { icon: Droplets, label: "Humidity", - value: `${analytics.humidity}%`, + value: analytics?.humidity ? `${analytics.humidity}%` : "N/A", color: "text-blue-500 dark:text-blue-300", bg: "bg-blue-50 dark:bg-blue-900", }, { icon: Sun, label: "Sunlight", - value: `${analytics.sunlight}%`, + value: analytics?.sunlight ? `${analytics.sunlight}%` : "N/A", color: "text-yellow-500 dark:text-yellow-300", bg: "bg-yellow-50 dark:bg-yellow-900", }, { icon: Leaf, label: "Soil Moisture", - value: `${analytics.soilMoisture}%`, + value: analytics?.soilMoisture ? `${analytics.soilMoisture}%` : "N/A", color: "text-green-500 dark:text-green-300", bg: "bg-green-50 dark:bg-green-900", }, { icon: Wind, label: "Wind Speed", - value: analytics.windSpeed, + value: analytics?.windSpeed ?? "N/A", color: "text-gray-500 dark:text-gray-300", bg: "bg-gray-50 dark:bg-gray-900", }, { icon: CloudRain, label: "Rainfall", - value: analytics.rainfall, + value: analytics?.rainfall ?? "N/A", color: "text-indigo-500 dark:text-indigo-300", bg: "bg-indigo-50 dark:bg-indigo-900", }, ].map((metric) => ( - + -
-
- +
+
+
-

- {metric.label} -

-

- {metric.value} -

+

{metric.label}

+

{metric.value}

))}
- - {/* Growth Progress */}
Growth Progress - - {analytics.growthProgress}% - + {growthProgress}%
- + +

+ Based on {daysToMaturity ? `${daysToMaturity} days` : "N/A"} to maturity. +

- {/* Next Action Card */} - +
-
- +
+
-

- Next Action Required -

+

Next Action Required

- {analytics.nextAction} -

-

- Due by{" "} - {analytics.nextActionDue.toLocaleDateString()} + {analytics?.nextAction || "Check crop status"}

+ {analytics?.nextActionDue && ( +

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

+ )} + {!analytics?.nextAction && ( +

No immediate actions required.

+ )}
@@ -349,13 +391,18 @@ export default function CropDetailPage({ {/* Map Section */} - + - Field Map - View and manage crop location + Cropland Location / Boundary + Visual representation on the farm - - + + {/* TODO: Ensure GoogleMapWithDrawing correctly parses and displays GeoFeatureData */} +
@@ -363,83 +410,64 @@ export default function CropDetailPage({ {/* Right Column */}
{/* Nutrient Levels */} - + - Nutrient Levels - Current soil composition + + + Nutrient Levels + + Soil composition (if available)
{[ { name: "Nitrogen (N)", - value: analytics.nutrientLevels.nitrogen, + value: analytics?.nutrientLevels?.nitrogen, color: "bg-blue-500 dark:bg-blue-700", }, { name: "Phosphorus (P)", - value: analytics.nutrientLevels.phosphorus, + value: analytics?.nutrientLevels?.phosphorus, color: "bg-yellow-500 dark:bg-yellow-700", }, { name: "Potassium (K)", - value: analytics.nutrientLevels.potassium, + value: analytics?.nutrientLevels?.potassium, color: "bg-green-500 dark:bg-green-700", }, ].map((nutrient) => (
{nutrient.name} - - {nutrient.value}% - + {nutrient.value ?? "N/A"}%
))} + {!analytics?.nutrientLevels && ( +

Nutrient data not available.

+ )}
- {/* Recent Activity */} - + - Recent Activity - Latest updates and changes + + + Recent Activity + + Latest updates (placeholder) - {[...Array(5)].map((_, i) => ( -
-
-
- -
-
-

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

-

- 2 hours ago -

-
-
- {i < 4 && ( - - )} -
- ))} +
No recent activity logged.
@@ -447,38 +475,30 @@ export default function CropDetailPage({
{/* Dialogs */} - - + + {/* Ensure AnalyticsDialog uses the correct props */} + {analytics && ( + + )}
); } - -/** - * 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/app/(sidebar)/farms/[farmId]/page.tsx b/frontend/app/(sidebar)/farms/[farmId]/page.tsx index 6e776f8..ab7bd2d 100644 --- a/frontend/app/(sidebar)/farms/[farmId]/page.tsx +++ b/frontend/app/(sidebar)/farms/[farmId]/page.tsx @@ -1,463 +1,406 @@ -// "use client"; +"use client"; -// import React, { useState, useEffect } from "react"; -// import { useRouter } from "next/navigation"; -// import { -// ArrowLeft, -// MapPin, -// Plus, -// Sprout, -// Calendar, -// LayoutGrid, -// AlertTriangle, -// Loader2, -// Home, -// ChevronRight, -// Droplets, -// Sun, -// Wind, -// } from "lucide-react"; -// import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; -// import { Button } from "@/components/ui/button"; -// import { CropDialog } from "./crop-dialog"; -// import { CropCard } from "./crop-card"; -// import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; -// import { Badge } from "@/components/ui/badge"; -// import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -// import { motion, AnimatePresence } from "framer-motion"; -// import type { Farm, Crop } from "@/types"; -// import { fetchFarmDetails } from "@/api/farm"; +import React, { useMemo, useState } from "react"; +import { useRouter } from "next/navigation"; +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import { + ArrowLeft, + MapPin, + Plus, + Sprout, + Calendar, + LayoutGrid, + AlertTriangle, + Loader2, + Home, + ChevronRight, + Sun, +} from "lucide-react"; +import { motion, AnimatePresence } from "framer-motion"; +import { useParams } from "next/navigation"; -// /** -// * Used in Next.js; params is now a Promise and must be unwrapped with React.use() -// */ -// interface FarmDetailPageProps { -// params: Promise<{ farmId: string }>; -// } +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { CropDialog } from "./crop-dialog"; +import { CropCard } from "./crop-card"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { Badge } from "@/components/ui/badge"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import type { Farm, Cropland } from "@/types"; +import { getCropsByFarmId, createCrop, CropResponse } from "@/api/crop"; +import { getFarm } from "@/api/farm"; -// export default function FarmDetailPage({ params }: FarmDetailPageProps) { -// // Unwrap the promised params using React.use() (experimental) -// const resolvedParams = React.use(params); +// =================================================================== +// Page Component: FarmDetailPage +// - Manages farm details, crop listings, filter tabs, and crop creation. +// - Performs API requests via React Query. +// =================================================================== +export default function FarmDetailPage() { + // --------------------------------------------------------------- + // Routing and URL Params + // --------------------------------------------------------------- + const params = useParams<{ farmId: string }>(); + const farmId = params.farmId; + const router = useRouter(); + const queryClient = useQueryClient(); -// const router = useRouter(); -// const [farm, setFarm] = useState(null); -// const [crops, setCrops] = useState([]); -// const [isDialogOpen, setIsDialogOpen] = useState(false); -// const [isLoading, setIsLoading] = useState(true); -// const [error, setError] = useState(null); -// const [activeFilter, setActiveFilter] = useState("all"); + // --------------------------------------------------------------- + // Local State for dialog and crop filter management + // --------------------------------------------------------------- + const [isDialogOpen, setIsDialogOpen] = useState(false); + const [activeFilter, setActiveFilter] = useState("all"); -// // Fetch farm details on initial render using the resolved params -// useEffect(() => { -// async function loadFarmDetails() { -// try { -// setIsLoading(true); -// setError(null); -// const { farm, crops } = await fetchFarmDetails(resolvedParams.farmId); -// setFarm(farm); -// setCrops(crops); -// } catch (err) { -// if (err instanceof Error) { -// if (err.message === "FARM_NOT_FOUND") { -// router.push("/not-found"); -// return; -// } -// setError(err.message); -// } else { -// setError("An unknown error occurred"); -// } -// } finally { -// setIsLoading(false); -// } -// } + // --------------------------------------------------------------- + // Data fetching: Farm details and Crops using React Query. + // - See: https://tanstack.com/query + // --------------------------------------------------------------- + const { + data: farm, + isLoading: isLoadingFarm, + isError: isErrorFarm, + error: errorFarm, + } = useQuery({ + queryKey: ["farm", farmId], + queryFn: () => getFarm(farmId), + enabled: !!farmId, + staleTime: 60 * 1000, + }); -// loadFarmDetails(); -// }, [resolvedParams.farmId, router]); + const { + data: cropData, // Changed name to avoid conflict + isLoading: isLoadingCrops, + isError: isErrorCrops, + error: errorCrops, + } = useQuery({ + // Use CropResponse type + queryKey: ["crops", farmId], + queryFn: () => getCropsByFarmId(farmId), // Use updated API function name + enabled: !!farmId, + staleTime: 60 * 1000, + }); -// /** -// * Handles adding a new crop. -// */ -// const handleAddCrop = async (data: Partial) => { -// try { -// // Simulate API delay -// await new Promise((resolve) => setTimeout(resolve, 800)); + // --------------------------------------------------------------- + // Mutation: Create Crop + // - After creation, invalidate queries to refresh data. + // --------------------------------------------------------------- + const croplands = useMemo(() => cropData?.croplands || [], [cropData]); -// const newCrop: Crop = { -// id: Math.random().toString(36).substr(2, 9), -// farmId: farm!.id, -// name: data.name!, -// plantedDate: data.plantedDate!, -// status: data.status!, -// variety: data.variety || "Standard", -// area: data.area || "0 hectares", -// healthScore: data.status === "growing" ? 85 : 0, -// progress: data.status === "growing" ? 10 : 0, -// }; + const mutation = useMutation({ + mutationFn: (newCropData: Partial) => createCrop({ ...newCropData, farmId: farmId }), // Pass farmId here + onSuccess: (newlyCreatedCrop) => { + console.log("Successfully created crop:", newlyCreatedCrop); + queryClient.invalidateQueries({ queryKey: ["crops", farmId] }); + queryClient.invalidateQueries({ queryKey: ["farm", farmId] }); // Invalidate farm too to update crop count potentially + setIsDialogOpen(false); + }, + onError: (error) => { + console.error("Failed to add crop:", error); + // TODO: Show user-friendly error message (e.g., using toast) + }, + }); -// setCrops((prev) => [newCrop, ...prev]); + const handleAddCrop = async (data: Partial) => { + await mutation.mutateAsync(data); + }; -// // Update the farm's crop count -// if (farm) { -// setFarm({ ...farm, crops: farm.crops + 1 }); -// } + // --------------------------------------------------------------- + // Determine combined loading and error states from individual queries. + // --------------------------------------------------------------- + const isLoading = isLoadingFarm || isLoadingCrops; + const isError = isErrorFarm || isErrorCrops; + const error = errorFarm || errorCrops; -// setIsDialogOpen(false); -// } catch (err) { -// setError("Failed to add crop. Please try again."); -// } -// }; + // --------------------------------------------------------------- + // Filter crops based on the active filter tab. + // --------------------------------------------------------------- + const filteredCrops = useMemo(() => { + // Renamed from filteredCrops + return croplands.filter( + (crop) => activeFilter === "all" || crop.status.toLowerCase() === activeFilter.toLowerCase() // Use camelCase status + ); + }, [croplands, activeFilter]); -// // Filter crops based on the active filter -// const filteredCrops = crops.filter((crop) => activeFilter === "all" || crop.status === activeFilter); + // --------------------------------------------------------------- + // Calculate counts for each crop status to display in tabs. + // --------------------------------------------------------------- + const possibleStatuses = ["growing", "planned", "harvested", "fallow"]; // Use lowercase + const cropCounts = useMemo(() => { + return croplands.reduce( + (acc, crop) => { + const status = crop.status.toLowerCase(); // Use camelCase status + if (acc[status] !== undefined) { + acc[status]++; + } else { + acc["other"] = (acc["other"] || 0) + 1; // Count unknown statuses + } + acc.all++; + return acc; + }, + { all: 0, ...Object.fromEntries(possibleStatuses.map((s) => [s, 0])) } as Record + ); + }, [croplands]); -// // Calculate crop counts grouped by status -// const cropCounts = { -// all: crops.length, -// growing: crops.filter((crop) => crop.status === "growing").length, -// planned: crops.filter((crop) => crop.status === "planned").length, -// harvested: crops.filter((crop) => crop.status === "harvested").length, -// }; + // --------------------------------------------------------------- + // Derive the unique statuses from the crops list for the tabs. + // --------------------------------------------------------------- + const availableStatuses = useMemo(() => { + return ["all", ...new Set(croplands.map((crop) => crop.status.toLowerCase()))]; // Use camelCase status + }, [croplands]); -// return ( -//
-//
-//
-// {/* Breadcrumbs */} -// + // =================================================================== + // Render: Main page layout segmented into breadcrumbs, farm cards, + // crop management, and the crop add dialog. + // =================================================================== + return ( +
+
+
+ {/* ------------------------------ + Breadcrumbs Navigation Section + ------------------------------ */} + -// {/* Back button */} -// + {/* ------------------------------ + Back Navigation Button + ------------------------------ */} + -// {/* Error state */} -// {error && ( -// -// -// Error -// {error} -// -// )} + {/* ------------------------------ + Error and Loading States + ------------------------------ */} + {isError && !isLoadingFarm && !farm && ( + + + Error Loading Farm + {(error as Error)?.message || "Could not load farm details."} + + )} + {isErrorCrops && ( + + + Error Loading Crops + {(errorCrops as Error)?.message || "Could not load crop data."} + + )} + {isLoading && ( +
+ +

Loading farm details...

+
+ )} -// {/* Loading state */} -// {isLoading && ( -//
-// -//

Loading farm details...

-//
-// )} + {/* ------------------------------ + Farm Details and Statistics + ------------------------------ */} + {!isLoadingFarm && !isErrorFarm && farm && ( + <> +
+ {/* Farm Info Card */} + + +
+ + {farm.farmType} + +
+ + Created {new Date(farm.createdAt).toLocaleDateString()} +
+
+
+
+ +
+
+

{farm.name}

+
+ + Lat: {farm.lat?.toFixed(4)}, Lon: {farm.lon?.toFixed(4)} +
+
+
+
+ +
+
+

Total Area

+

{farm.totalSize}

+
+
+

Total Crops

+

{isLoadingCrops ? "..." : cropCounts.all ?? 0}

+
+
+

Growing

+

{isLoadingCrops ? "..." : cropCounts.growing ?? 0}

+
+
+

Harvested

+

{isLoadingCrops ? "..." : cropCounts.harvested ?? 0}

+
+
+
+
-// {/* Farm details */} -// {!isLoading && !error && farm && ( -// <> -//
-// {/* Farm info card */} -// -// -//
-// -// {farm.type} -// -//
-// -// Created {farm.createdAt.toLocaleDateString()} -//
-//
-//
-//
-// -//
-//
-//

{farm.name}

-//
-// -// {farm.location} -//
-//
-//
-//
-// -//
-//
-//

Total Area

-//

{farm.area}

-//
-//
-//

Total Crops

-//

{farm.crops}

-//
-//
-//

Growing Crops

-//

{cropCounts.growing}

-//
-//
-//

Harvested

-//

{cropCounts.harvested}

-//
-//
-//
-//
+ {/* Weather Overview Card */} + + + + Weather Overview + + Current conditions + + +
+ Temperature + 25°C +
+
+ Humidity + 60% +
+
+ Wind + 10 km/h +
+
+ Rainfall (24h) + 2 mm +
+
+
+
-// {/* Weather card */} -// -// -// Current Conditions -// Weather at your farm location -// -// -//
-//
-//
-// -//
-//
-//

Temperature

-//

{farm.weather?.temperature}°C

-//
-//
-//
-//
-// -//
-//
-//

Humidity

-//

{farm.weather?.humidity}%

-//
-//
-//
-//
-// -//
-//
-//

Sunlight

-//

{farm.weather?.sunlight}%

-//
-//
-//
-//
-// -//
-//
-//

Rainfall

-//

{farm.weather?.rainfall}

-//
-//
-//
-//
-//
-//
+ {/* ------------------------------ + Crops Section: List and Filtering Tabs + ------------------------------ */} +
+
+
+

+ + Crops / Croplands +

+

Manage and monitor all croplands in this farm

+
+ +
+ {mutation.isError && ( + + + Failed to Add Crop + + {(mutation.error as Error)?.message || "Could not add the crop. Please try again."} + + + )} -// {/* Crops section */} -//
-//
-//
-//

-// -// Crops -//

-//

Manage and monitor all crops in this farm

-//
-// -//
+ + + {availableStatuses.map((status) => ( + + {status === "all" ? "All" : status} ({isLoadingCrops ? "..." : cropCounts[status] ?? 0}) + + ))} + -// -// -// setActiveFilter("all")}> -// All Crops ({cropCounts.all}) -// -// setActiveFilter("growing")}> -// Growing ({cropCounts.growing}) -// -// setActiveFilter("planned")}> -// Planned ({cropCounts.planned}) -// -// setActiveFilter("harvested")}> -// Harvested ({cropCounts.harvested}) -// -// + {isLoadingCrops ? ( +
+ +
+ ) : isErrorCrops ? ( +
Failed to load crops.
+ ) : ( + availableStatuses.map((status) => ( + + {filteredCrops.length === 0 && activeFilter === status ? ( +
+
+ +
+

+ No {status === "all" ? "" : status} crops found +

+

+ {status === "all" + ? "You haven't added any crops to this farm yet." + : `No crops with status "${status}" found.`} +

+ +
+ ) : activeFilter === status && filteredCrops.length > 0 ? ( +
+ + {filteredCrops.map((crop, index) => ( + + router.push(`/farms/${farmId}/crops/${crop.uuid}`)} + /> + + ))} + +
+ ) : null} +
+ )) + )} +
+
+ + )} +
+
-// -// {filteredCrops.length === 0 ? ( -//
-//
-// -//
-//

No crops found

-//

-// {activeFilter === "all" -// ? "You haven't added any crops to this farm yet." -// : `No ${activeFilter} crops found. Try a different filter.`} -//

-// -//
-// ) : ( -//
-// -// {filteredCrops.map((crop, index) => ( -// -// router.push(`/farms/${crop.farmId}/crops/${crop.id}`)} -// /> -// -// ))} -// -//
-// )} -//
- -// {/* Growing tab */} -// -// {filteredCrops.length === 0 ? ( -//
-//
-// -//
-//

No growing crops

-//

-// You don't have any growing crops in this farm yet. -//

-// -//
-// ) : ( -//
-// -// {filteredCrops.map((crop, index) => ( -// -// router.push(`/farms/${crop.farmId}/crops/${crop.id}`)} -// /> -// -// ))} -// -//
-// )} -//
- -// {/* Planned tab */} -// -// {filteredCrops.length === 0 ? ( -//
-//

No planned crops

-//

-// You don't have any planned crops in this farm yet. -//

-// -//
-// ) : ( -//
-// -// {filteredCrops.map((crop, index) => ( -// -// router.push(`/farms/${crop.farmId}/crops/${crop.id}`)} -// /> -// -// ))} -// -//
-// )} -//
- -// {/* Harvested tab */} -// -// {filteredCrops.length === 0 ? ( -//
-//

No harvested crops

-//

-// You don't have any harvested crops in this farm yet. -//

-// -//
-// ) : ( -//
-// -// {filteredCrops.map((crop, index) => ( -// -// router.push(`/farms/${crop.farmId}/crops/${crop.id}`)} -// /> -// -// ))} -// -//
-// )} -//
-// -//
-// -// )} -//
-//
- -// {/* Add Crop Dialog */} -// -//
-// ); -// } - -export default function FarmDetailPage({ params }: FarmDetailPageProps) { - return
hello
; + {/* ------------------------------ + Add Crop Dialog Component + - Passes the mutation state to display loading indicators. + ------------------------------ */} + +
+ ); } diff --git a/frontend/app/(sidebar)/farms/add-farm-form.tsx b/frontend/app/(sidebar)/farms/add-farm-form.tsx index 97fc717..b01b6cf 100644 --- a/frontend/app/(sidebar)/farms/add-farm-form.tsx +++ b/frontend/app/(sidebar)/farms/add-farm-form.tsx @@ -71,11 +71,11 @@ export function AddFarmForm({ onSubmit, onCancel }: AddFarmFormProps) { try { setIsSubmitting(true); const farmData: Partial = { - Name: values.name, - Lat: values.latitude, - Lon: values.longitude, - FarmType: values.type, - TotalSize: values.area, + name: values.name, + lat: values.latitude, + lon: values.longitude, + farmType: values.type, + totalSize: values.area, }; await onSubmit(farmData); form.reset(); diff --git a/frontend/app/(sidebar)/farms/farm-card.tsx b/frontend/app/(sidebar)/farms/farm-card.tsx index 537d36b..00090ae 100644 --- a/frontend/app/(sidebar)/farms/farm-card.tsx +++ b/frontend/app/(sidebar)/farms/farm-card.tsx @@ -9,7 +9,7 @@ import type { Farm } from "@/types"; export interface FarmCardProps { variant: "farm" | "add"; - farm?: Farm; + farm?: Farm; // Use updated Farm type onClick?: () => void; } @@ -40,7 +40,7 @@ export function FarmCard({ variant, farm, onClick }: FarmCardProps) { year: "numeric", month: "short", day: "numeric", - }).format(new Date(farm.CreatedAt)); + }).format(new Date(farm.createdAt)); return ( @@ -49,7 +49,7 @@ export function FarmCard({ variant, farm, onClick }: FarmCardProps) { - {farm.FarmType} + {farm.farmType}
@@ -63,19 +63,19 @@ export function FarmCard({ variant, farm, onClick }: FarmCardProps) {
-

{farm.Name}

+

{farm.name}

- {farm.Lat} + {farm.lat}

Area

-

{farm.TotalSize}

+

{farm.totalSize}

Crops

-

{farm.Crops ? farm.Crops.length : 0}

+

{farm.crops ? farm.crops.length : 0}

diff --git a/frontend/app/(sidebar)/farms/page.tsx b/frontend/app/(sidebar)/farms/page.tsx index 04ed915..cef3fe3 100644 --- a/frontend/app/(sidebar)/farms/page.tsx +++ b/frontend/app/(sidebar)/farms/page.tsx @@ -36,18 +36,21 @@ export default function FarmSetupPage() { const [isDialogOpen, setIsDialogOpen] = useState(false); const { - data: farms, + data: farms, // Type is Farm[] now isLoading, isError, error, } = useQuery({ + // Use Farm[] type queryKey: ["farms"], queryFn: fetchFarms, staleTime: 60 * 1000, }); const mutation = useMutation({ - mutationFn: (data: Partial) => createFarm(data), + // Pass the correct type to createFarm + mutationFn: (data: Partial>) => + createFarm(data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["farms"] }); setIsDialogOpen(false); @@ -69,23 +72,23 @@ export default function FarmSetupPage() { const filteredAndSortedFarms = (farms || []) .filter( (farm) => - (activeFilter === "all" || farm.FarmType === activeFilter) && - (farm.Name.toLowerCase().includes(searchQuery.toLowerCase()) || - // farm.location.toLowerCase().includes(searchQuery.toLowerCase()) || - farm.FarmType.toLowerCase().includes(searchQuery.toLowerCase())) + (activeFilter === "all" || farm.farmType === activeFilter) && // Use camelCase farmType + (farm.name.toLowerCase().includes(searchQuery.toLowerCase()) || // Use camelCase name + // farm.location is no longer a single string, use lat/lon if needed for search + farm.farmType.toLowerCase().includes(searchQuery.toLowerCase())) // Use camelCase farmType ) .sort((a, b) => { if (sortOrder === "newest") { - return new Date(b.CreatedAt).getTime() - new Date(a.CreatedAt).getTime(); + return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(); // Use camelCase createdAt } else if (sortOrder === "oldest") { - return new Date(a.CreatedAt).getTime() - new Date(b.CreatedAt).getTime(); + return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(); // Use camelCase createdAt } else { - return a.Name.localeCompare(b.Name); + return a.name.localeCompare(b.name); // Use camelCase name } }); // Get distinct farm types. - const farmTypes = ["all", ...new Set((farms || []).map((farm) => farm.FarmType))]; + const farmTypes = ["all", ...new Set((farms || []).map((farm) => farm.farmType))]; // Use camelCase farmType const handleAddFarm = async (data: Partial) => { await mutation.mutateAsync(data); @@ -133,6 +136,7 @@ export default function FarmSetupPage() { ))}
+ {/* DropdownMenu remains the same, Check icon was missing */}