diff --git a/frontend/api/farm.ts b/frontend/api/farm.ts index 30af22d..4e18614 100644 --- a/frontend/api/farm.ts +++ b/frontend/api/farm.ts @@ -1,171 +1,198 @@ +import axiosInstance from "./config"; 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. + * Fetch a specific crop by id using axios. + * Falls back to dummy data on error. */ 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, - }); + try { + const response = await axiosInstance.get(`/api/crops/${id}`); + return response.data; + } catch (error) { + // Fallback dummy data + return { + 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. + * Fetch crop analytics by crop id using axios. + * Returns dummy analytics if the API call fails. */ 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, - }, - }); + try { + const response = await axiosInstance.get(`/api/crops/${id}/analytics`); + return response.data; + } catch (error) { + return { + 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. + * Fetch an array of farms using axios. + * Simulates a delay and a random error; returns dummy data if the API is unavailable. */ 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."); + try { + const response = await axiosInstance.get("/api/farms"); + return response.data; + } catch (error) { + // Optionally, you could simulate a random error here. For now we return fallback data. + 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, + }, + ]; } - - 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. + * Simulates creating a new farm. + * Waits for 800ms and then uses dummy data. + */ +export async function createFarm(data: Partial): Promise { + await new Promise((resolve) => setTimeout(resolve, 800)); + // In a real implementation you might call: + // const response = await axiosInstance.post("/api/farms", data); + // return response.data; + return { + id: Math.random().toString(36).substr(2, 9), + name: data.name!, + location: data.location!, + type: data.type!, + createdAt: new Date(), + area: data.area || "0 hectares", + crops: 0, + }; +} + +// Additional functions for fetching crop details remain unchanged... + +/** + * Fetch detailed information for a specific farm (including its crops) using axios. + * If the API call fails, returns fallback dummy data. */ 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."); + try { + const response = await axiosInstance.get<{ farm: Farm; crops: Crop[] }>(`/api/farms/${farmId}`); + return response.data; + } catch (error) { + // If the given farmId is "999", simulate a not found error. + 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, + // Additional details such as weather can be included if needed. + 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 }; } - - // 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/add-farm-form.tsx b/frontend/app/(sidebar)/farms/add-farm-form.tsx index 840bece..3a23e92 100644 --- a/frontend/app/(sidebar)/farms/add-farm-form.tsx +++ b/frontend/app/(sidebar)/farms/add-farm-form.tsx @@ -18,7 +18,7 @@ const farmFormSchema = z.object({ area: z.string().optional(), }); -interface AddFarmFormProps { +export interface AddFarmFormProps { onSubmit: (data: Partial) => Promise; onCancel: () => void; } diff --git a/frontend/app/(sidebar)/farms/farm-card.tsx b/frontend/app/(sidebar)/farms/farm-card.tsx index 50f99c2..40afe47 100644 --- a/frontend/app/(sidebar)/farms/farm-card.tsx +++ b/frontend/app/(sidebar)/farms/farm-card.tsx @@ -63,7 +63,7 @@ export function FarmCard({ variant, farm, onClick }: FarmCardProps) {
-

{farm.name}

+

{farm.name}

{farm.location} diff --git a/frontend/app/(sidebar)/farms/page.tsx b/frontend/app/(sidebar)/farms/page.tsx index af9b4b9..957b321 100644 --- a/frontend/app/(sidebar)/farms/page.tsx +++ b/frontend/app/(sidebar)/farms/page.tsx @@ -1,9 +1,10 @@ "use client"; -import { useState, useEffect } from "react"; +import { useState } from "react"; import { useRouter } from "next/navigation"; import { motion, AnimatePresence } from "framer-motion"; import { Search, Plus, Filter, SlidersHorizontal, Leaf, Calendar, AlertTriangle, Loader2 } from "lucide-react"; +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog"; import { Separator } from "@/components/ui/separator"; @@ -23,70 +24,37 @@ import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { FarmCard } from "./farm-card"; import { AddFarmForm } from "./add-farm-form"; import type { Farm } from "@/types"; -import { fetchFarms } from "@/api/farm"; +import { fetchFarms, createFarm } from "@/api/farm"; -/** - * FarmSetupPage component allows users to search, filter, sort, and add farms. - */ export default function FarmSetupPage() { const router = useRouter(); + const queryClient = useQueryClient(); - // Component state - const [isDialogOpen, setIsDialogOpen] = useState(false); const [searchQuery, setSearchQuery] = useState(""); - const [farms, setFarms] = useState([]); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); const [activeFilter, setActiveFilter] = useState("all"); const [sortOrder, setSortOrder] = useState<"newest" | "oldest" | "alphabetical">("newest"); + const [isDialogOpen, setIsDialogOpen] = useState(false); - // Load farms when the component mounts. - useEffect(() => { - async function loadFarms() { - try { - setIsLoading(true); - setError(null); - const data = await fetchFarms(); - setFarms(data); - } catch (err) { - setError(err instanceof Error ? err.message : "An unknown error occurred"); - } finally { - setIsLoading(false); - } - } + const { + data: farms, + isLoading, + isError, + error, + } = useQuery({ + queryKey: ["farms"], + queryFn: fetchFarms, + staleTime: 60 * 1000, + }); - loadFarms(); - }, []); - - /** - * Handles adding a new farm. - * - * @param data - Partial Farm data from the form. - */ - const handleAddFarm = async (data: Partial) => { - try { - // Simulate an API call delay for adding a new farm. - await new Promise((resolve) => setTimeout(resolve, 800)); - - const newFarm: Farm = { - id: Math.random().toString(36).substr(2, 9), - name: data.name!, - location: data.location!, - type: data.type!, - createdAt: new Date(), - area: data.area || "0 hectares", - crops: 0, - }; - - setFarms((prev) => [newFarm, ...prev]); + const mutation = useMutation({ + mutationFn: (data: Partial) => createFarm(data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["farms"] }); setIsDialogOpen(false); - } catch (err) { - setError("Failed to add farm. Please try again."); - } - }; + }, + }); - // Filter and sort farms based on the current state. - const filteredAndSortedFarms = farms + const filteredAndSortedFarms = (farms || []) .filter( (farm) => (activeFilter === "all" || farm.type === activeFilter) && @@ -96,16 +64,20 @@ export default function FarmSetupPage() { ) .sort((a, b) => { if (sortOrder === "newest") { - return b.createdAt.getTime() - a.createdAt.getTime(); + return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(); } else if (sortOrder === "oldest") { - return a.createdAt.getTime() - b.createdAt.getTime(); + return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(); } else { return a.name.localeCompare(b.name); } }); - // Get available farm types for filters. - const farmTypes = ["all", ...new Set(farms.map((farm) => farm.type))]; + // Get distinct farm types. + const farmTypes = ["all", ...new Set((farms || []).map((farm) => farm.type))]; + + const handleAddFarm = async (data: Partial) => { + await mutation.mutateAsync(data); + }; return (
@@ -187,11 +159,11 @@ export default function FarmSetupPage() { {/* Error state */} - {error && ( + {isError && ( Error - {error} + {(error as Error)?.message} )} @@ -204,7 +176,7 @@ export default function FarmSetupPage() { )} {/* Empty state */} - {!isLoading && !error && filteredAndSortedFarms.length === 0 && ( + {!isLoading && !isError && filteredAndSortedFarms.length === 0 && (
@@ -223,7 +195,7 @@ export default function FarmSetupPage() { onClick={() => { setSearchQuery(""); setActiveFilter("all"); - if (!farms.length) { + if (!farms || farms.length === 0) { setIsDialogOpen(true); } }} @@ -241,7 +213,7 @@ export default function FarmSetupPage() { )} {/* Grid of farm cards */} - {!isLoading && !error && filteredAndSortedFarms.length > 0 && ( + {!isLoading && !isError && filteredAndSortedFarms.length > 0 && (
setIsDialogOpen(true)} /> - {filteredAndSortedFarms.map((farm, index) => (
By {blog.author}
- +