mirror of
https://github.com/ForFarmTeam/ForFarm.git
synced 2025-12-19 14:04:08 +01:00
feat: add farm setup and fetch
This commit is contained in:
parent
21559f3b55
commit
6885eb4bfb
@ -1,198 +1,100 @@
|
|||||||
import axiosInstance from "./config";
|
import axiosInstance from "./config";
|
||||||
import type { Crop, CropAnalytics, Farm } from "@/types";
|
import type { Farm } from "@/types";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch a specific crop by id using axios.
|
* Fetch an array of farms.
|
||||||
* Falls back to dummy data on error.
|
* Calls GET /farms and returns fallback dummy data on failure.
|
||||||
*/
|
|
||||||
export async function fetchCropById(id: string): Promise<Crop> {
|
|
||||||
try {
|
|
||||||
const response = await axiosInstance.get<Crop>(`/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 crop analytics by crop id using axios.
|
|
||||||
* Returns dummy analytics if the API call fails.
|
|
||||||
*/
|
|
||||||
export async function fetchAnalyticsByCropId(id: string): Promise<CropAnalytics> {
|
|
||||||
try {
|
|
||||||
const response = await axiosInstance.get<CropAnalytics>(`/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,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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<Farm[]> {
|
export async function fetchFarms(): Promise<Farm[]> {
|
||||||
// Simulate network delay
|
return axiosInstance.get<Farm[]>("/farms").then((res) => res.data);
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await axiosInstance.get<Farm[]>("/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,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Simulates creating a new farm.
|
* Create a new farm.
|
||||||
* Waits for 800ms and then uses dummy data.
|
* Calls POST /farms with a payload that uses snake_case keys.
|
||||||
*/
|
*/
|
||||||
export async function createFarm(data: Partial<Farm>): Promise<Farm> {
|
export async function createFarm(data: Partial<Farm>): Promise<Farm> {
|
||||||
await new Promise((resolve) => setTimeout(resolve, 800));
|
return axiosInstance.post<Farm>("/farms", data).then((res) => res.data);
|
||||||
// In a real implementation you might call:
|
|
||||||
// const response = await axiosInstance.post<Farm>("/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.
|
* Fetch a specific farm by ID.
|
||||||
* If the API call fails, returns fallback dummy data.
|
* Calls GET /farms/{farm_id} and returns fallback data on failure.
|
||||||
*/
|
*/
|
||||||
export async function fetchFarmDetails(farmId: string): Promise<{ farm: Farm; crops: Crop[] }> {
|
export async function getFarm(farmId: string): Promise<Farm> {
|
||||||
// Simulate network delay
|
// Simulate a network delay.
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1200));
|
await new Promise((resolve) => setTimeout(resolve, 600));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await axiosInstance.get<{ farm: Farm; crops: Crop[] }>(`/api/farms/${farmId}`);
|
const response = await axiosInstance.get<Farm>(`/farms/${farmId}`);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
// If the given farmId is "999", simulate a not found error.
|
console.error(`Error fetching farm ${farmId}. Returning fallback data:`, error);
|
||||||
if (farmId === "999") {
|
const dummyDate = new Date().toISOString();
|
||||||
throw new Error("FARM_NOT_FOUND");
|
return {
|
||||||
}
|
CreatedAt: dummyDate,
|
||||||
|
FarmType: "conventional",
|
||||||
const farm: Farm = {
|
Lat: 15.87,
|
||||||
id: farmId,
|
Lon: 100.9925,
|
||||||
name: "Green Valley Farm",
|
Name: "Fallback Farm",
|
||||||
location: "Bangkok, Thailand",
|
OwnerID: "fallback_owner",
|
||||||
type: "durian",
|
TotalSize: "40 hectares",
|
||||||
createdAt: new Date("2023-01-15"),
|
UUID: farmId,
|
||||||
area: "12.5 hectares",
|
UpdatedAt: dummyDate,
|
||||||
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,
|
* Update an existing farm.
|
||||||
name: "Monthong Durian",
|
* Calls PUT /farms/{farm_id} with a snake_case payload.
|
||||||
plantedDate: new Date("2023-03-15"),
|
*/
|
||||||
status: "growing",
|
export async function updateFarm(
|
||||||
variety: "Premium",
|
farmId: string,
|
||||||
area: "4.2 hectares",
|
data: {
|
||||||
healthScore: 92,
|
farm_type: string;
|
||||||
progress: 65,
|
lat: number;
|
||||||
},
|
lon: number;
|
||||||
{
|
name: string;
|
||||||
id: "2",
|
total_size: string;
|
||||||
farmId,
|
}
|
||||||
name: "Chanee Durian",
|
): Promise<Farm> {
|
||||||
plantedDate: new Date("2023-02-20"),
|
// Simulate a network delay.
|
||||||
status: "planned",
|
await new Promise((resolve) => setTimeout(resolve, 800));
|
||||||
variety: "Standard",
|
|
||||||
area: "3.8 hectares",
|
try {
|
||||||
healthScore: 0,
|
const response = await axiosInstance.put<Farm>(`/farms/${farmId}`, data);
|
||||||
progress: 0,
|
return response.data;
|
||||||
},
|
} catch (error: any) {
|
||||||
{
|
console.error(`Error updating farm ${farmId}. Returning fallback data:`, error);
|
||||||
id: "3",
|
const now = new Date().toISOString();
|
||||||
farmId,
|
return {
|
||||||
name: "Kradum Durian",
|
CreatedAt: now,
|
||||||
plantedDate: new Date("2022-11-05"),
|
FarmType: data.farm_type,
|
||||||
status: "harvested",
|
Lat: data.lat,
|
||||||
variety: "Premium",
|
Lon: data.lon,
|
||||||
area: "4.5 hectares",
|
Name: data.name,
|
||||||
healthScore: 100,
|
OwnerID: "updated_owner",
|
||||||
progress: 100,
|
TotalSize: data.total_size,
|
||||||
},
|
UUID: farmId,
|
||||||
];
|
UpdatedAt: now,
|
||||||
|
};
|
||||||
return { farm, crops };
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a specific farm.
|
||||||
|
* Calls DELETE /farms/{farm_id} and returns a success message.
|
||||||
|
*/
|
||||||
|
export async function deleteFarm(farmId: string): Promise<{ message: string }> {
|
||||||
|
// Simulate a network delay.
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||||
|
|
||||||
|
try {
|
||||||
|
await axiosInstance.delete(`/farms/${farmId}`);
|
||||||
|
return { message: "Farm deleted successfully" };
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(`Error deleting farm ${farmId}. Assuming deletion was successful:`, error);
|
||||||
|
return { message: "Farm deleted successfully (dummy)" };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,459 +1,463 @@
|
|||||||
"use client";
|
// "use client";
|
||||||
|
|
||||||
import React, { useState, useEffect } from "react";
|
// import React, { useState, useEffect } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
// import { useRouter } from "next/navigation";
|
||||||
import {
|
// import {
|
||||||
ArrowLeft,
|
// ArrowLeft,
|
||||||
MapPin,
|
// MapPin,
|
||||||
Plus,
|
// Plus,
|
||||||
Sprout,
|
// Sprout,
|
||||||
Calendar,
|
// Calendar,
|
||||||
LayoutGrid,
|
// LayoutGrid,
|
||||||
AlertTriangle,
|
// AlertTriangle,
|
||||||
Loader2,
|
// Loader2,
|
||||||
Home,
|
// Home,
|
||||||
ChevronRight,
|
// ChevronRight,
|
||||||
Droplets,
|
// Droplets,
|
||||||
Sun,
|
// Sun,
|
||||||
Wind,
|
// Wind,
|
||||||
} from "lucide-react";
|
// } 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 { Button } from "@/components/ui/button";
|
||||||
import { CropDialog } from "./crop-dialog";
|
// import { CropDialog } from "./crop-dialog";
|
||||||
import { CropCard } from "./crop-card";
|
// import { CropCard } from "./crop-card";
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
// import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||||
import { Badge } from "@/components/ui/badge";
|
// import { Badge } from "@/components/ui/badge";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
// import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import { motion, AnimatePresence } from "framer-motion";
|
// import { motion, AnimatePresence } from "framer-motion";
|
||||||
import type { Farm, Crop } from "@/types";
|
// import type { Farm, Crop } from "@/types";
|
||||||
import { fetchFarmDetails } from "@/api/farm";
|
// import { fetchFarmDetails } from "@/api/farm";
|
||||||
|
|
||||||
/**
|
// /**
|
||||||
* Used in Next.js; params is now a Promise and must be unwrapped with React.use()
|
// * Used in Next.js; params is now a Promise and must be unwrapped with React.use()
|
||||||
*/
|
// */
|
||||||
interface FarmDetailPageProps {
|
// interface FarmDetailPageProps {
|
||||||
params: Promise<{ farmId: string }>;
|
// params: Promise<{ farmId: string }>;
|
||||||
}
|
// }
|
||||||
|
|
||||||
|
// export default function FarmDetailPage({ params }: FarmDetailPageProps) {
|
||||||
|
// // Unwrap the promised params using React.use() (experimental)
|
||||||
|
// const resolvedParams = React.use(params);
|
||||||
|
|
||||||
|
// const router = useRouter();
|
||||||
|
// const [farm, setFarm] = useState<Farm | null>(null);
|
||||||
|
// const [crops, setCrops] = useState<Crop[]>([]);
|
||||||
|
// const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||||
|
// const [isLoading, setIsLoading] = useState(true);
|
||||||
|
// const [error, setError] = useState<string | null>(null);
|
||||||
|
// const [activeFilter, setActiveFilter] = useState<string>("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);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// loadFarmDetails();
|
||||||
|
// }, [resolvedParams.farmId, router]);
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Handles adding a new crop.
|
||||||
|
// */
|
||||||
|
// const handleAddCrop = async (data: Partial<Crop>) => {
|
||||||
|
// try {
|
||||||
|
// // Simulate API delay
|
||||||
|
// await new Promise((resolve) => setTimeout(resolve, 800));
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
// };
|
||||||
|
|
||||||
|
// setCrops((prev) => [newCrop, ...prev]);
|
||||||
|
|
||||||
|
// // Update the farm's crop count
|
||||||
|
// if (farm) {
|
||||||
|
// setFarm({ ...farm, crops: farm.crops + 1 });
|
||||||
|
// }
|
||||||
|
|
||||||
|
// setIsDialogOpen(false);
|
||||||
|
// } catch (err) {
|
||||||
|
// setError("Failed to add crop. Please try again.");
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
// // Filter crops based on the active filter
|
||||||
|
// const filteredCrops = crops.filter((crop) => activeFilter === "all" || crop.status === activeFilter);
|
||||||
|
|
||||||
|
// // 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,
|
||||||
|
// };
|
||||||
|
|
||||||
|
// return (
|
||||||
|
// <div className="min-h-screen bg-background text-foreground">
|
||||||
|
// <div className="container max-w-7xl p-6 mx-auto">
|
||||||
|
// <div className="flex flex-col gap-6">
|
||||||
|
// {/* Breadcrumbs */}
|
||||||
|
// <nav className="flex items-center text-sm text-muted-foreground">
|
||||||
|
// <Button
|
||||||
|
// variant="link"
|
||||||
|
// className="p-0 h-auto font-normal text-muted-foreground"
|
||||||
|
// onClick={() => router.push("/")}>
|
||||||
|
// <Home className="h-3.5 w-3.5 mr-1" />
|
||||||
|
// Home
|
||||||
|
// </Button>
|
||||||
|
// <ChevronRight className="h-3.5 w-3.5 mx-1" />
|
||||||
|
// <Button
|
||||||
|
// variant="link"
|
||||||
|
// className="p-0 h-auto font-normal text-muted-foreground"
|
||||||
|
// onClick={() => router.push("/farms")}>
|
||||||
|
// Farms
|
||||||
|
// </Button>
|
||||||
|
// <ChevronRight className="h-3.5 w-3.5 mx-1" />
|
||||||
|
// <span className="text-foreground font-medium truncate">{farm?.name || "Farm Details"}</span>
|
||||||
|
// </nav>
|
||||||
|
|
||||||
|
// {/* Back button */}
|
||||||
|
// <Button
|
||||||
|
// variant="outline"
|
||||||
|
// size="sm"
|
||||||
|
// className="w-fit gap-2 text-muted-foreground"
|
||||||
|
// onClick={() => router.push("/farms")}>
|
||||||
|
// <ArrowLeft className="h-4 w-4" /> Back to Farms
|
||||||
|
// </Button>
|
||||||
|
|
||||||
|
// {/* Error state */}
|
||||||
|
// {error && (
|
||||||
|
// <Alert variant="destructive">
|
||||||
|
// <AlertTriangle className="h-4 w-4" />
|
||||||
|
// <AlertTitle>Error</AlertTitle>
|
||||||
|
// <AlertDescription>{error}</AlertDescription>
|
||||||
|
// </Alert>
|
||||||
|
// )}
|
||||||
|
|
||||||
|
// {/* Loading state */}
|
||||||
|
// {isLoading && (
|
||||||
|
// <div className="flex flex-col items-center justify-center py-12">
|
||||||
|
// <Loader2 className="h-8 w-8 text-green-600 animate-spin mb-4" />
|
||||||
|
// <p className="text-muted-foreground">Loading farm details...</p>
|
||||||
|
// </div>
|
||||||
|
// )}
|
||||||
|
|
||||||
|
// {/* Farm details */}
|
||||||
|
// {!isLoading && !error && farm && (
|
||||||
|
// <>
|
||||||
|
// <div className="grid gap-6 md:grid-cols-12">
|
||||||
|
// {/* Farm info card */}
|
||||||
|
// <Card className="md:col-span-8">
|
||||||
|
// <CardHeader className="pb-2">
|
||||||
|
// <div className="flex items-center justify-between">
|
||||||
|
// <Badge
|
||||||
|
// variant="outline"
|
||||||
|
// className="capitalize bg-green-50 dark:bg-green-900 text-green-700 dark:text-green-300 border-green-200">
|
||||||
|
// {farm.type}
|
||||||
|
// </Badge>
|
||||||
|
// <div className="flex items-center text-sm text-muted-foreground">
|
||||||
|
// <Calendar className="h-4 w-4 mr-1" />
|
||||||
|
// Created {farm.createdAt.toLocaleDateString()}
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
// <div className="flex items-start gap-4 mt-2">
|
||||||
|
// <div className="h-12 w-12 rounded-full bg-green-100 dark:bg-green-800 flex items-center justify-center">
|
||||||
|
// <Sprout className="h-6 w-6 text-green-600 dark:text-green-300" />
|
||||||
|
// </div>
|
||||||
|
// <div>
|
||||||
|
// <h1 className="text-2xl font-bold">{farm.name}</h1>
|
||||||
|
// <div className="flex items-center text-muted-foreground mt-1">
|
||||||
|
// <MapPin className="h-4 w-4 mr-1" />
|
||||||
|
// {farm.location}
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
// </CardHeader>
|
||||||
|
// <CardContent>
|
||||||
|
// <div className="grid grid-cols-2 sm:grid-cols-4 gap-4 mt-2">
|
||||||
|
// <div className="bg-muted/30 dark:bg-muted/20 rounded-lg p-3">
|
||||||
|
// <p className="text-xs text-muted-foreground">Total Area</p>
|
||||||
|
// <p className="text-lg font-semibold">{farm.area}</p>
|
||||||
|
// </div>
|
||||||
|
// <div className="bg-muted/30 dark:bg-muted/20 rounded-lg p-3">
|
||||||
|
// <p className="text-xs text-muted-foreground">Total Crops</p>
|
||||||
|
// <p className="text-lg font-semibold">{farm.crops}</p>
|
||||||
|
// </div>
|
||||||
|
// <div className="bg-muted/30 dark:bg-muted/20 rounded-lg p-3">
|
||||||
|
// <p className="text-xs text-muted-foreground">Growing Crops</p>
|
||||||
|
// <p className="text-lg font-semibold">{cropCounts.growing}</p>
|
||||||
|
// </div>
|
||||||
|
// <div className="bg-muted/30 dark:bg-muted/20 rounded-lg p-3">
|
||||||
|
// <p className="text-xs text-muted-foreground">Harvested</p>
|
||||||
|
// <p className="text-lg font-semibold">{cropCounts.harvested}</p>
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
// </CardContent>
|
||||||
|
// </Card>
|
||||||
|
|
||||||
|
// {/* Weather card */}
|
||||||
|
// <Card className="md:col-span-4">
|
||||||
|
// <CardHeader>
|
||||||
|
// <CardTitle className="text-lg">Current Conditions</CardTitle>
|
||||||
|
// <CardDescription>Weather at your farm location</CardDescription>
|
||||||
|
// </CardHeader>
|
||||||
|
// <CardContent className="space-y-4">
|
||||||
|
// <div className="grid grid-cols-2 gap-4">
|
||||||
|
// <div className="flex items-start gap-2">
|
||||||
|
// <div className="p-2 rounded-lg bg-orange-50 dark:bg-orange-900">
|
||||||
|
// <Sun className="h-4 w-4 text-orange-500 dark:text-orange-200" />
|
||||||
|
// </div>
|
||||||
|
// <div>
|
||||||
|
// <p className="text-sm font-medium text-muted-foreground">Temperature</p>
|
||||||
|
// <p className="text-xl font-semibold">{farm.weather?.temperature}°C</p>
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
// <div className="flex items-start gap-2">
|
||||||
|
// <div className="p-2 rounded-lg bg-blue-50 dark:bg-blue-900">
|
||||||
|
// <Droplets className="h-4 w-4 text-blue-500 dark:text-blue-200" />
|
||||||
|
// </div>
|
||||||
|
// <div>
|
||||||
|
// <p className="text-sm font-medium text-muted-foreground">Humidity</p>
|
||||||
|
// <p className="text-xl font-semibold">{farm.weather?.humidity}%</p>
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
// <div className="flex items-start gap-2">
|
||||||
|
// <div className="p-2 rounded-lg bg-yellow-50 dark:bg-yellow-900">
|
||||||
|
// <Sun className="h-4 w-4 text-yellow-500 dark:text-yellow-200" />
|
||||||
|
// </div>
|
||||||
|
// <div>
|
||||||
|
// <p className="text-sm font-medium text-muted-foreground">Sunlight</p>
|
||||||
|
// <p className="text-xl font-semibold">{farm.weather?.sunlight}%</p>
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
// <div className="flex items-start gap-2">
|
||||||
|
// <div className="p-2 rounded-lg bg-gray-50 dark:bg-gray-900">
|
||||||
|
// <Wind className="h-4 w-4 text-gray-500 dark:text-gray-300" />
|
||||||
|
// </div>
|
||||||
|
// <div>
|
||||||
|
// <p className="text-sm font-medium text-muted-foreground">Rainfall</p>
|
||||||
|
// <p className="text-xl font-semibold">{farm.weather?.rainfall}</p>
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
// </CardContent>
|
||||||
|
// </Card>
|
||||||
|
// </div>
|
||||||
|
|
||||||
|
// {/* Crops section */}
|
||||||
|
// <div className="mt-4">
|
||||||
|
// <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-4">
|
||||||
|
// <div>
|
||||||
|
// <h2 className="text-xl font-bold flex items-center">
|
||||||
|
// <LayoutGrid className="h-5 w-5 mr-2 text-green-600 dark:text-green-300" />
|
||||||
|
// Crops
|
||||||
|
// </h2>
|
||||||
|
// <p className="text-sm text-muted-foreground">Manage and monitor all crops in this farm</p>
|
||||||
|
// </div>
|
||||||
|
// <Button
|
||||||
|
// onClick={() => setIsDialogOpen(true)}
|
||||||
|
// className="gap-2 bg-green-600 hover:bg-green-700 w-full sm:w-auto">
|
||||||
|
// <Plus className="h-4 w-4" />
|
||||||
|
// Add New Crop
|
||||||
|
// </Button>
|
||||||
|
// </div>
|
||||||
|
|
||||||
|
// <Tabs defaultValue="all" className="mt-6">
|
||||||
|
// <TabsList>
|
||||||
|
// <TabsTrigger value="all" onClick={() => setActiveFilter("all")}>
|
||||||
|
// All Crops ({cropCounts.all})
|
||||||
|
// </TabsTrigger>
|
||||||
|
// <TabsTrigger value="growing" onClick={() => setActiveFilter("growing")}>
|
||||||
|
// Growing ({cropCounts.growing})
|
||||||
|
// </TabsTrigger>
|
||||||
|
// <TabsTrigger value="planned" onClick={() => setActiveFilter("planned")}>
|
||||||
|
// Planned ({cropCounts.planned})
|
||||||
|
// </TabsTrigger>
|
||||||
|
// <TabsTrigger value="harvested" onClick={() => setActiveFilter("harvested")}>
|
||||||
|
// Harvested ({cropCounts.harvested})
|
||||||
|
// </TabsTrigger>
|
||||||
|
// </TabsList>
|
||||||
|
|
||||||
|
// <TabsContent value="all" className="mt-6">
|
||||||
|
// {filteredCrops.length === 0 ? (
|
||||||
|
// <div className="flex flex-col items-center justify-center py-12 bg-muted/20 dark:bg-muted/30 rounded-lg border border-dashed">
|
||||||
|
// <div className="bg-green-100 dark:bg-green-900 p-3 rounded-full mb-4">
|
||||||
|
// <Sprout className="h-6 w-6 text-green-600 dark:text-green-300" />
|
||||||
|
// </div>
|
||||||
|
// <h3 className="text-xl font-medium mb-2">No crops found</h3>
|
||||||
|
// <p className="text-muted-foreground text-center max-w-md mb-6">
|
||||||
|
// {activeFilter === "all"
|
||||||
|
// ? "You haven't added any crops to this farm yet."
|
||||||
|
// : `No ${activeFilter} crops found. Try a different filter.`}
|
||||||
|
// </p>
|
||||||
|
// <Button onClick={() => setIsDialogOpen(true)} className="gap-2">
|
||||||
|
// <Plus className="h-4 w-4" />
|
||||||
|
// Add your first crop
|
||||||
|
// </Button>
|
||||||
|
// </div>
|
||||||
|
// ) : (
|
||||||
|
// <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
||||||
|
// <AnimatePresence>
|
||||||
|
// {filteredCrops.map((crop, index) => (
|
||||||
|
// <motion.div
|
||||||
|
// key={crop.id}
|
||||||
|
// initial={{ opacity: 0, y: 20 }}
|
||||||
|
// animate={{ opacity: 1, y: 0 }}
|
||||||
|
// exit={{ opacity: 0, y: -20 }}
|
||||||
|
// transition={{ duration: 0.2, delay: index * 0.05 }}>
|
||||||
|
// <CropCard
|
||||||
|
// crop={crop}
|
||||||
|
// onClick={() => router.push(`/farms/${crop.farmId}/crops/${crop.id}`)}
|
||||||
|
// />
|
||||||
|
// </motion.div>
|
||||||
|
// ))}
|
||||||
|
// </AnimatePresence>
|
||||||
|
// </div>
|
||||||
|
// )}
|
||||||
|
// </TabsContent>
|
||||||
|
|
||||||
|
// {/* Growing tab */}
|
||||||
|
// <TabsContent value="growing" className="mt-6">
|
||||||
|
// {filteredCrops.length === 0 ? (
|
||||||
|
// <div className="flex flex-col items-center justify-center py-12 bg-muted/20 dark:bg-muted/30 rounded-lg border border-dashed">
|
||||||
|
// <div className="bg-green-100 dark:bg-green-900 p-3 rounded-full mb-4">
|
||||||
|
// <Sprout className="h-6 w-6 text-green-600 dark:text-green-300" />
|
||||||
|
// </div>
|
||||||
|
// <h3 className="text-xl font-medium mb-2">No growing crops</h3>
|
||||||
|
// <p className="text-muted-foreground text-center max-w-md mb-6">
|
||||||
|
// You don't have any growing crops in this farm yet.
|
||||||
|
// </p>
|
||||||
|
// <Button onClick={() => setIsDialogOpen(true)} className="gap-2">
|
||||||
|
// <Plus className="h-4 w-4" />
|
||||||
|
// Add a growing crop
|
||||||
|
// </Button>
|
||||||
|
// </div>
|
||||||
|
// ) : (
|
||||||
|
// <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
||||||
|
// <AnimatePresence>
|
||||||
|
// {filteredCrops.map((crop, index) => (
|
||||||
|
// <motion.div
|
||||||
|
// key={crop.id}
|
||||||
|
// initial={{ opacity: 0, y: 20 }}
|
||||||
|
// animate={{ opacity: 1, y: 0 }}
|
||||||
|
// exit={{ opacity: 0, y: -20 }}
|
||||||
|
// transition={{ duration: 0.2, delay: index * 0.05 }}>
|
||||||
|
// <CropCard
|
||||||
|
// crop={crop}
|
||||||
|
// onClick={() => router.push(`/farms/${crop.farmId}/crops/${crop.id}`)}
|
||||||
|
// />
|
||||||
|
// </motion.div>
|
||||||
|
// ))}
|
||||||
|
// </AnimatePresence>
|
||||||
|
// </div>
|
||||||
|
// )}
|
||||||
|
// </TabsContent>
|
||||||
|
|
||||||
|
// {/* Planned tab */}
|
||||||
|
// <TabsContent value="planned" className="mt-6">
|
||||||
|
// {filteredCrops.length === 0 ? (
|
||||||
|
// <div className="flex flex-col items-center justify-center py-12 bg-muted/20 dark:bg-muted/30 rounded-lg border border-dashed">
|
||||||
|
// <h3 className="text-xl font-medium mb-2">No planned crops</h3>
|
||||||
|
// <p className="text-muted-foreground text-center max-w-md mb-6">
|
||||||
|
// You don't have any planned crops in this farm yet.
|
||||||
|
// </p>
|
||||||
|
// <Button onClick={() => setIsDialogOpen(true)} className="gap-2">
|
||||||
|
// <Plus className="h-4 w-4" />
|
||||||
|
// Plan a new crop
|
||||||
|
// </Button>
|
||||||
|
// </div>
|
||||||
|
// ) : (
|
||||||
|
// <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
||||||
|
// <AnimatePresence>
|
||||||
|
// {filteredCrops.map((crop, index) => (
|
||||||
|
// <motion.div
|
||||||
|
// key={crop.id}
|
||||||
|
// initial={{ opacity: 0, y: 20 }}
|
||||||
|
// animate={{ opacity: 1, y: 0 }}
|
||||||
|
// exit={{ opacity: 0, y: -20 }}
|
||||||
|
// transition={{ duration: 0.2, delay: index * 0.05 }}>
|
||||||
|
// <CropCard
|
||||||
|
// crop={crop}
|
||||||
|
// onClick={() => router.push(`/farms/${crop.farmId}/crops/${crop.id}`)}
|
||||||
|
// />
|
||||||
|
// </motion.div>
|
||||||
|
// ))}
|
||||||
|
// </AnimatePresence>
|
||||||
|
// </div>
|
||||||
|
// )}
|
||||||
|
// </TabsContent>
|
||||||
|
|
||||||
|
// {/* Harvested tab */}
|
||||||
|
// <TabsContent value="harvested" className="mt-6">
|
||||||
|
// {filteredCrops.length === 0 ? (
|
||||||
|
// <div className="flex flex-col items-center justify-center py-12 bg-muted/20 dark:bg-muted/30 rounded-lg border border-dashed">
|
||||||
|
// <h3 className="text-xl font-medium mb-2">No harvested crops</h3>
|
||||||
|
// <p className="text-muted-foreground text-center max-w-md mb-6">
|
||||||
|
// You don't have any harvested crops in this farm yet.
|
||||||
|
// </p>
|
||||||
|
// <Button onClick={() => setActiveFilter("all")} className="gap-2">
|
||||||
|
// View all crops
|
||||||
|
// </Button>
|
||||||
|
// </div>
|
||||||
|
// ) : (
|
||||||
|
// <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
||||||
|
// <AnimatePresence>
|
||||||
|
// {filteredCrops.map((crop, index) => (
|
||||||
|
// <motion.div
|
||||||
|
// key={crop.id}
|
||||||
|
// initial={{ opacity: 0, y: 20 }}
|
||||||
|
// animate={{ opacity: 1, y: 0 }}
|
||||||
|
// exit={{ opacity: 0, y: -20 }}
|
||||||
|
// transition={{ duration: 0.2, delay: index * 0.05 }}>
|
||||||
|
// <CropCard
|
||||||
|
// crop={crop}
|
||||||
|
// onClick={() => router.push(`/farms/${crop.farmId}/crops/${crop.id}`)}
|
||||||
|
// />
|
||||||
|
// </motion.div>
|
||||||
|
// ))}
|
||||||
|
// </AnimatePresence>
|
||||||
|
// </div>
|
||||||
|
// )}
|
||||||
|
// </TabsContent>
|
||||||
|
// </Tabs>
|
||||||
|
// </div>
|
||||||
|
// </>
|
||||||
|
// )}
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
|
||||||
|
// {/* Add Crop Dialog */}
|
||||||
|
// <CropDialog open={isDialogOpen} onOpenChange={setIsDialogOpen} onSubmit={handleAddCrop} />
|
||||||
|
// </div>
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
export default function FarmDetailPage({ params }: FarmDetailPageProps) {
|
export default function FarmDetailPage({ params }: FarmDetailPageProps) {
|
||||||
// Unwrap the promised params using React.use() (experimental)
|
return <div>hello</div>;
|
||||||
const resolvedParams = React.use(params);
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const [farm, setFarm] = useState<Farm | null>(null);
|
|
||||||
const [crops, setCrops] = useState<Crop[]>([]);
|
|
||||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
const [activeFilter, setActiveFilter] = useState<string>("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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
loadFarmDetails();
|
|
||||||
}, [resolvedParams.farmId, router]);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles adding a new crop.
|
|
||||||
*/
|
|
||||||
const handleAddCrop = async (data: Partial<Crop>) => {
|
|
||||||
try {
|
|
||||||
// Simulate API delay
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 800));
|
|
||||||
|
|
||||||
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,
|
|
||||||
};
|
|
||||||
|
|
||||||
setCrops((prev) => [newCrop, ...prev]);
|
|
||||||
|
|
||||||
// Update the farm's crop count
|
|
||||||
if (farm) {
|
|
||||||
setFarm({ ...farm, crops: farm.crops + 1 });
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsDialogOpen(false);
|
|
||||||
} catch (err) {
|
|
||||||
setError("Failed to add crop. Please try again.");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Filter crops based on the active filter
|
|
||||||
const filteredCrops = crops.filter((crop) => activeFilter === "all" || crop.status === activeFilter);
|
|
||||||
|
|
||||||
// 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,
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-background text-foreground">
|
|
||||||
<div className="container max-w-7xl p-6 mx-auto">
|
|
||||||
<div className="flex flex-col gap-6">
|
|
||||||
{/* Breadcrumbs */}
|
|
||||||
<nav className="flex items-center text-sm text-muted-foreground">
|
|
||||||
<Button
|
|
||||||
variant="link"
|
|
||||||
className="p-0 h-auto font-normal text-muted-foreground"
|
|
||||||
onClick={() => router.push("/")}>
|
|
||||||
<Home className="h-3.5 w-3.5 mr-1" />
|
|
||||||
Home
|
|
||||||
</Button>
|
|
||||||
<ChevronRight className="h-3.5 w-3.5 mx-1" />
|
|
||||||
<Button
|
|
||||||
variant="link"
|
|
||||||
className="p-0 h-auto font-normal text-muted-foreground"
|
|
||||||
onClick={() => router.push("/farms")}>
|
|
||||||
Farms
|
|
||||||
</Button>
|
|
||||||
<ChevronRight className="h-3.5 w-3.5 mx-1" />
|
|
||||||
<span className="text-foreground font-medium truncate">{farm?.name || "Farm Details"}</span>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
{/* Back button */}
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
className="w-fit gap-2 text-muted-foreground"
|
|
||||||
onClick={() => router.push("/farms")}>
|
|
||||||
<ArrowLeft className="h-4 w-4" /> Back to Farms
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{/* Error state */}
|
|
||||||
{error && (
|
|
||||||
<Alert variant="destructive">
|
|
||||||
<AlertTriangle className="h-4 w-4" />
|
|
||||||
<AlertTitle>Error</AlertTitle>
|
|
||||||
<AlertDescription>{error}</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Loading state */}
|
|
||||||
{isLoading && (
|
|
||||||
<div className="flex flex-col items-center justify-center py-12">
|
|
||||||
<Loader2 className="h-8 w-8 text-green-600 animate-spin mb-4" />
|
|
||||||
<p className="text-muted-foreground">Loading farm details...</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Farm details */}
|
|
||||||
{!isLoading && !error && farm && (
|
|
||||||
<>
|
|
||||||
<div className="grid gap-6 md:grid-cols-12">
|
|
||||||
{/* Farm info card */}
|
|
||||||
<Card className="md:col-span-8">
|
|
||||||
<CardHeader className="pb-2">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<Badge
|
|
||||||
variant="outline"
|
|
||||||
className="capitalize bg-green-50 dark:bg-green-900 text-green-700 dark:text-green-300 border-green-200">
|
|
||||||
{farm.type}
|
|
||||||
</Badge>
|
|
||||||
<div className="flex items-center text-sm text-muted-foreground">
|
|
||||||
<Calendar className="h-4 w-4 mr-1" />
|
|
||||||
Created {farm.createdAt.toLocaleDateString()}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-start gap-4 mt-2">
|
|
||||||
<div className="h-12 w-12 rounded-full bg-green-100 dark:bg-green-800 flex items-center justify-center">
|
|
||||||
<Sprout className="h-6 w-6 text-green-600 dark:text-green-300" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h1 className="text-2xl font-bold">{farm.name}</h1>
|
|
||||||
<div className="flex items-center text-muted-foreground mt-1">
|
|
||||||
<MapPin className="h-4 w-4 mr-1" />
|
|
||||||
{farm.location}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4 mt-2">
|
|
||||||
<div className="bg-muted/30 dark:bg-muted/20 rounded-lg p-3">
|
|
||||||
<p className="text-xs text-muted-foreground">Total Area</p>
|
|
||||||
<p className="text-lg font-semibold">{farm.area}</p>
|
|
||||||
</div>
|
|
||||||
<div className="bg-muted/30 dark:bg-muted/20 rounded-lg p-3">
|
|
||||||
<p className="text-xs text-muted-foreground">Total Crops</p>
|
|
||||||
<p className="text-lg font-semibold">{farm.crops}</p>
|
|
||||||
</div>
|
|
||||||
<div className="bg-muted/30 dark:bg-muted/20 rounded-lg p-3">
|
|
||||||
<p className="text-xs text-muted-foreground">Growing Crops</p>
|
|
||||||
<p className="text-lg font-semibold">{cropCounts.growing}</p>
|
|
||||||
</div>
|
|
||||||
<div className="bg-muted/30 dark:bg-muted/20 rounded-lg p-3">
|
|
||||||
<p className="text-xs text-muted-foreground">Harvested</p>
|
|
||||||
<p className="text-lg font-semibold">{cropCounts.harvested}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Weather card */}
|
|
||||||
<Card className="md:col-span-4">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="text-lg">Current Conditions</CardTitle>
|
|
||||||
<CardDescription>Weather at your farm location</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-4">
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div className="flex items-start gap-2">
|
|
||||||
<div className="p-2 rounded-lg bg-orange-50 dark:bg-orange-900">
|
|
||||||
<Sun className="h-4 w-4 text-orange-500 dark:text-orange-200" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-sm font-medium text-muted-foreground">Temperature</p>
|
|
||||||
<p className="text-xl font-semibold">{farm.weather?.temperature}°C</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-start gap-2">
|
|
||||||
<div className="p-2 rounded-lg bg-blue-50 dark:bg-blue-900">
|
|
||||||
<Droplets className="h-4 w-4 text-blue-500 dark:text-blue-200" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-sm font-medium text-muted-foreground">Humidity</p>
|
|
||||||
<p className="text-xl font-semibold">{farm.weather?.humidity}%</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-start gap-2">
|
|
||||||
<div className="p-2 rounded-lg bg-yellow-50 dark:bg-yellow-900">
|
|
||||||
<Sun className="h-4 w-4 text-yellow-500 dark:text-yellow-200" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-sm font-medium text-muted-foreground">Sunlight</p>
|
|
||||||
<p className="text-xl font-semibold">{farm.weather?.sunlight}%</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-start gap-2">
|
|
||||||
<div className="p-2 rounded-lg bg-gray-50 dark:bg-gray-900">
|
|
||||||
<Wind className="h-4 w-4 text-gray-500 dark:text-gray-300" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-sm font-medium text-muted-foreground">Rainfall</p>
|
|
||||||
<p className="text-xl font-semibold">{farm.weather?.rainfall}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Crops section */}
|
|
||||||
<div className="mt-4">
|
|
||||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-4">
|
|
||||||
<div>
|
|
||||||
<h2 className="text-xl font-bold flex items-center">
|
|
||||||
<LayoutGrid className="h-5 w-5 mr-2 text-green-600 dark:text-green-300" />
|
|
||||||
Crops
|
|
||||||
</h2>
|
|
||||||
<p className="text-sm text-muted-foreground">Manage and monitor all crops in this farm</p>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
onClick={() => setIsDialogOpen(true)}
|
|
||||||
className="gap-2 bg-green-600 hover:bg-green-700 w-full sm:w-auto">
|
|
||||||
<Plus className="h-4 w-4" />
|
|
||||||
Add New Crop
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Tabs defaultValue="all" className="mt-6">
|
|
||||||
<TabsList>
|
|
||||||
<TabsTrigger value="all" onClick={() => setActiveFilter("all")}>
|
|
||||||
All Crops ({cropCounts.all})
|
|
||||||
</TabsTrigger>
|
|
||||||
<TabsTrigger value="growing" onClick={() => setActiveFilter("growing")}>
|
|
||||||
Growing ({cropCounts.growing})
|
|
||||||
</TabsTrigger>
|
|
||||||
<TabsTrigger value="planned" onClick={() => setActiveFilter("planned")}>
|
|
||||||
Planned ({cropCounts.planned})
|
|
||||||
</TabsTrigger>
|
|
||||||
<TabsTrigger value="harvested" onClick={() => setActiveFilter("harvested")}>
|
|
||||||
Harvested ({cropCounts.harvested})
|
|
||||||
</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
|
|
||||||
<TabsContent value="all" className="mt-6">
|
|
||||||
{filteredCrops.length === 0 ? (
|
|
||||||
<div className="flex flex-col items-center justify-center py-12 bg-muted/20 dark:bg-muted/30 rounded-lg border border-dashed">
|
|
||||||
<div className="bg-green-100 dark:bg-green-900 p-3 rounded-full mb-4">
|
|
||||||
<Sprout className="h-6 w-6 text-green-600 dark:text-green-300" />
|
|
||||||
</div>
|
|
||||||
<h3 className="text-xl font-medium mb-2">No crops found</h3>
|
|
||||||
<p className="text-muted-foreground text-center max-w-md mb-6">
|
|
||||||
{activeFilter === "all"
|
|
||||||
? "You haven't added any crops to this farm yet."
|
|
||||||
: `No ${activeFilter} crops found. Try a different filter.`}
|
|
||||||
</p>
|
|
||||||
<Button onClick={() => setIsDialogOpen(true)} className="gap-2">
|
|
||||||
<Plus className="h-4 w-4" />
|
|
||||||
Add your first crop
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
|
||||||
<AnimatePresence>
|
|
||||||
{filteredCrops.map((crop, index) => (
|
|
||||||
<motion.div
|
|
||||||
key={crop.id}
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
exit={{ opacity: 0, y: -20 }}
|
|
||||||
transition={{ duration: 0.2, delay: index * 0.05 }}>
|
|
||||||
<CropCard
|
|
||||||
crop={crop}
|
|
||||||
onClick={() => router.push(`/farms/${crop.farmId}/crops/${crop.id}`)}
|
|
||||||
/>
|
|
||||||
</motion.div>
|
|
||||||
))}
|
|
||||||
</AnimatePresence>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
{/* Growing tab */}
|
|
||||||
<TabsContent value="growing" className="mt-6">
|
|
||||||
{filteredCrops.length === 0 ? (
|
|
||||||
<div className="flex flex-col items-center justify-center py-12 bg-muted/20 dark:bg-muted/30 rounded-lg border border-dashed">
|
|
||||||
<div className="bg-green-100 dark:bg-green-900 p-3 rounded-full mb-4">
|
|
||||||
<Sprout className="h-6 w-6 text-green-600 dark:text-green-300" />
|
|
||||||
</div>
|
|
||||||
<h3 className="text-xl font-medium mb-2">No growing crops</h3>
|
|
||||||
<p className="text-muted-foreground text-center max-w-md mb-6">
|
|
||||||
You don't have any growing crops in this farm yet.
|
|
||||||
</p>
|
|
||||||
<Button onClick={() => setIsDialogOpen(true)} className="gap-2">
|
|
||||||
<Plus className="h-4 w-4" />
|
|
||||||
Add a growing crop
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
|
||||||
<AnimatePresence>
|
|
||||||
{filteredCrops.map((crop, index) => (
|
|
||||||
<motion.div
|
|
||||||
key={crop.id}
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
exit={{ opacity: 0, y: -20 }}
|
|
||||||
transition={{ duration: 0.2, delay: index * 0.05 }}>
|
|
||||||
<CropCard
|
|
||||||
crop={crop}
|
|
||||||
onClick={() => router.push(`/farms/${crop.farmId}/crops/${crop.id}`)}
|
|
||||||
/>
|
|
||||||
</motion.div>
|
|
||||||
))}
|
|
||||||
</AnimatePresence>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
{/* Planned tab */}
|
|
||||||
<TabsContent value="planned" className="mt-6">
|
|
||||||
{filteredCrops.length === 0 ? (
|
|
||||||
<div className="flex flex-col items-center justify-center py-12 bg-muted/20 dark:bg-muted/30 rounded-lg border border-dashed">
|
|
||||||
<h3 className="text-xl font-medium mb-2">No planned crops</h3>
|
|
||||||
<p className="text-muted-foreground text-center max-w-md mb-6">
|
|
||||||
You don't have any planned crops in this farm yet.
|
|
||||||
</p>
|
|
||||||
<Button onClick={() => setIsDialogOpen(true)} className="gap-2">
|
|
||||||
<Plus className="h-4 w-4" />
|
|
||||||
Plan a new crop
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
|
||||||
<AnimatePresence>
|
|
||||||
{filteredCrops.map((crop, index) => (
|
|
||||||
<motion.div
|
|
||||||
key={crop.id}
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
exit={{ opacity: 0, y: -20 }}
|
|
||||||
transition={{ duration: 0.2, delay: index * 0.05 }}>
|
|
||||||
<CropCard
|
|
||||||
crop={crop}
|
|
||||||
onClick={() => router.push(`/farms/${crop.farmId}/crops/${crop.id}`)}
|
|
||||||
/>
|
|
||||||
</motion.div>
|
|
||||||
))}
|
|
||||||
</AnimatePresence>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
{/* Harvested tab */}
|
|
||||||
<TabsContent value="harvested" className="mt-6">
|
|
||||||
{filteredCrops.length === 0 ? (
|
|
||||||
<div className="flex flex-col items-center justify-center py-12 bg-muted/20 dark:bg-muted/30 rounded-lg border border-dashed">
|
|
||||||
<h3 className="text-xl font-medium mb-2">No harvested crops</h3>
|
|
||||||
<p className="text-muted-foreground text-center max-w-md mb-6">
|
|
||||||
You don't have any harvested crops in this farm yet.
|
|
||||||
</p>
|
|
||||||
<Button onClick={() => setActiveFilter("all")} className="gap-2">
|
|
||||||
View all crops
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
|
||||||
<AnimatePresence>
|
|
||||||
{filteredCrops.map((crop, index) => (
|
|
||||||
<motion.div
|
|
||||||
key={crop.id}
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
exit={{ opacity: 0, y: -20 }}
|
|
||||||
transition={{ duration: 0.2, delay: index * 0.05 }}>
|
|
||||||
<CropCard
|
|
||||||
crop={crop}
|
|
||||||
onClick={() => router.push(`/farms/${crop.farmId}/crops/${crop.id}`)}
|
|
||||||
/>
|
|
||||||
</motion.div>
|
|
||||||
))}
|
|
||||||
</AnimatePresence>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</TabsContent>
|
|
||||||
</Tabs>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Add Crop Dialog */}
|
|
||||||
<CropDialog open={isDialogOpen} onOpenChange={setIsDialogOpen} onSubmit={handleAddCrop} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,10 +10,12 @@ import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, For
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Loader2 } from "lucide-react";
|
import { Loader2 } from "lucide-react";
|
||||||
import type { Farm } from "@/types";
|
import type { Farm } from "@/types";
|
||||||
|
import GoogleMapWithDrawing from "@/components/google-map-with-drawing";
|
||||||
|
|
||||||
const farmFormSchema = z.object({
|
const farmFormSchema = z.object({
|
||||||
name: z.string().min(2, "Farm name must be at least 2 characters"),
|
name: z.string().min(2, "Farm name must be at least 2 characters"),
|
||||||
location: z.string().min(2, "Location must be at least 2 characters"),
|
latitude: z.number().min(-90, "Invalid latitude").max(90, "Invalid latitude"),
|
||||||
|
longitude: z.number().min(-180, "Invalid longitude").max(180, "Invalid longitude"),
|
||||||
type: z.string().min(1, "Please select a farm type"),
|
type: z.string().min(1, "Please select a farm type"),
|
||||||
area: z.string().optional(),
|
area: z.string().optional(),
|
||||||
});
|
});
|
||||||
@ -30,7 +32,8 @@ export function AddFarmForm({ onSubmit, onCancel }: AddFarmFormProps) {
|
|||||||
resolver: zodResolver(farmFormSchema),
|
resolver: zodResolver(farmFormSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: "",
|
name: "",
|
||||||
location: "",
|
latitude: 0,
|
||||||
|
longitude: 0,
|
||||||
type: "",
|
type: "",
|
||||||
area: "",
|
area: "",
|
||||||
},
|
},
|
||||||
@ -39,7 +42,14 @@ export function AddFarmForm({ onSubmit, onCancel }: AddFarmFormProps) {
|
|||||||
const handleSubmit = async (values: z.infer<typeof farmFormSchema>) => {
|
const handleSubmit = async (values: z.infer<typeof farmFormSchema>) => {
|
||||||
try {
|
try {
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
await onSubmit(values);
|
const farmData: Partial<Farm> = {
|
||||||
|
Name: values.name,
|
||||||
|
Lat: values.latitude,
|
||||||
|
Lon: values.longitude,
|
||||||
|
FarmType: values.type,
|
||||||
|
TotalSize: values.area,
|
||||||
|
};
|
||||||
|
await onSubmit(farmData);
|
||||||
form.reset();
|
form.reset();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error submitting form:", error);
|
console.error("Error submitting form:", error);
|
||||||
@ -48,95 +58,127 @@ export function AddFarmForm({ onSubmit, onCancel }: AddFarmFormProps) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleAreaSelected = (coordinates: { lat: number; lng: number }[]) => {
|
||||||
|
if (coordinates.length > 0) {
|
||||||
|
const { lat, lng } = coordinates[0];
|
||||||
|
form.setValue("latitude", lat);
|
||||||
|
form.setValue("longitude", lng);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form {...form}>
|
<div className="flex flex-col lg:flex-row gap-6">
|
||||||
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-6">
|
{/* Form Section */}
|
||||||
<FormField
|
<div className="flex-1">
|
||||||
control={form.control}
|
<Form {...form}>
|
||||||
name="name"
|
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-6">
|
||||||
render={({ field }) => (
|
<FormField
|
||||||
<FormItem>
|
control={form.control}
|
||||||
<FormLabel>Farm Name</FormLabel>
|
name="name"
|
||||||
<FormControl>
|
render={({ field }) => (
|
||||||
<Input placeholder="Enter farm name" {...field} />
|
<FormItem>
|
||||||
</FormControl>
|
<FormLabel>Farm Name</FormLabel>
|
||||||
<FormDescription>This is your farm's display name.</FormDescription>
|
<FormControl>
|
||||||
<FormMessage />
|
<Input placeholder="Enter farm name" {...field} />
|
||||||
</FormItem>
|
</FormControl>
|
||||||
)}
|
<FormDescription>This is your farm's display name.</FormDescription>
|
||||||
/>
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="location"
|
name="latitude"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Location</FormLabel>
|
<FormLabel>Latitude</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="Enter farm location" {...field} />
|
<Input placeholder="Latitude" {...field} disabled />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>City, region or specific address</FormDescription>
|
<FormMessage />
|
||||||
<FormMessage />
|
</FormItem>
|
||||||
</FormItem>
|
)}
|
||||||
)}
|
/>
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="type"
|
name="longitude"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Farm Type</FormLabel>
|
<FormLabel>Longitude</FormLabel>
|
||||||
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
<FormControl>
|
||||||
<FormControl>
|
<Input placeholder="Longitude" {...field} disabled />
|
||||||
<SelectTrigger>
|
</FormControl>
|
||||||
<SelectValue placeholder="Select farm type" />
|
<FormMessage />
|
||||||
</SelectTrigger>
|
</FormItem>
|
||||||
</FormControl>
|
)}
|
||||||
<SelectContent>
|
/>
|
||||||
<SelectItem value="durian">Durian</SelectItem>
|
|
||||||
<SelectItem value="mango">Mango</SelectItem>
|
|
||||||
<SelectItem value="rice">Rice</SelectItem>
|
|
||||||
<SelectItem value="mixed">Mixed Crops</SelectItem>
|
|
||||||
<SelectItem value="other">Other</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="area"
|
name="type"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Total Area (optional)</FormLabel>
|
<FormLabel>Farm Type</FormLabel>
|
||||||
<FormControl>
|
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
||||||
<Input placeholder="e.g., 10 hectares" {...field} />
|
<FormControl>
|
||||||
</FormControl>
|
<SelectTrigger>
|
||||||
<FormDescription>The total size of your farm</FormDescription>
|
<SelectValue placeholder="Select farm type" />
|
||||||
<FormMessage />
|
</SelectTrigger>
|
||||||
</FormItem>
|
</FormControl>
|
||||||
)}
|
<SelectContent>
|
||||||
/>
|
<SelectItem value="durian">Durian</SelectItem>
|
||||||
|
<SelectItem value="mango">Mango</SelectItem>
|
||||||
|
<SelectItem value="rice">Rice</SelectItem>
|
||||||
|
<SelectItem value="mixed">Mixed Crops</SelectItem>
|
||||||
|
<SelectItem value="other">Other</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="flex justify-end gap-2">
|
<FormField
|
||||||
<Button type="button" variant="outline" onClick={onCancel} disabled={isSubmitting}>
|
control={form.control}
|
||||||
Cancel
|
name="area"
|
||||||
</Button>
|
render={({ field }) => (
|
||||||
<Button type="submit" disabled={isSubmitting} className="bg-green-600 hover:bg-green-700">
|
<FormItem>
|
||||||
{isSubmitting ? (
|
<FormLabel>Total Area (optional)</FormLabel>
|
||||||
<>
|
<FormControl>
|
||||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
<Input placeholder="e.g., 10 hectares" {...field} />
|
||||||
Creating...
|
</FormControl>
|
||||||
</>
|
<FormDescription>The total size of your farm</FormDescription>
|
||||||
) : (
|
<FormMessage />
|
||||||
"Create Farm"
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
</Button>
|
/>
|
||||||
</div>
|
|
||||||
</form>
|
<div className="flex justify-end gap-2">
|
||||||
</Form>
|
<Button type="button" variant="outline" onClick={onCancel} disabled={isSubmitting}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" disabled={isSubmitting} className="bg-green-600 hover:bg-green-700">
|
||||||
|
{isSubmitting ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
Creating...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
"Create Farm"
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Map Section */}
|
||||||
|
<div className="flex-1">
|
||||||
|
<FormLabel>Farm Location</FormLabel>
|
||||||
|
<GoogleMapWithDrawing onAreaSelected={handleAreaSelected} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -40,7 +40,7 @@ export function FarmCard({ variant, farm, onClick }: FarmCardProps) {
|
|||||||
year: "numeric",
|
year: "numeric",
|
||||||
month: "short",
|
month: "short",
|
||||||
day: "numeric",
|
day: "numeric",
|
||||||
}).format(farm.createdAt);
|
}).format(new Date(farm.CreatedAt));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className={cardClasses} onClick={onClick}>
|
<Card className={cardClasses} onClick={onClick}>
|
||||||
@ -49,7 +49,7 @@ export function FarmCard({ variant, farm, onClick }: FarmCardProps) {
|
|||||||
<Badge
|
<Badge
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="capitalize bg-green-50 dark:bg-green-900 text-green-700 dark:text-green-300 border-green-200">
|
className="capitalize bg-green-50 dark:bg-green-900 text-green-700 dark:text-green-300 border-green-200">
|
||||||
{farm.type}
|
{farm.FarmType}
|
||||||
</Badge>
|
</Badge>
|
||||||
<div className="flex items-center text-xs text-muted-foreground">
|
<div className="flex items-center text-xs text-muted-foreground">
|
||||||
<CalendarDays className="h-3 w-3 mr-1" />
|
<CalendarDays className="h-3 w-3 mr-1" />
|
||||||
@ -63,15 +63,15 @@ export function FarmCard({ variant, farm, onClick }: FarmCardProps) {
|
|||||||
<Sprout className="h-5 w-5 text-green-600" />
|
<Sprout className="h-5 w-5 text-green-600" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-xl font-medium mb-1 truncate">{farm.name}</h3>
|
<h3 className="text-xl font-medium mb-1 truncate">{farm.Name}</h3>
|
||||||
<div className="flex items-center text-sm text-muted-foreground mb-2">
|
<div className="flex items-center text-sm text-muted-foreground mb-2">
|
||||||
<MapPin className="h-3.5 w-3.5 mr-1 flex-shrink-0" />
|
<MapPin className="h-3.5 w-3.5 mr-1 flex-shrink-0" />
|
||||||
<span className="truncate">{farm.location}</span>
|
<span className="truncate">{farm.Lat}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-2 mt-3">
|
<div className="grid grid-cols-2 gap-2 mt-3">
|
||||||
<div className="bg-muted/30 dark:bg-muted/20 rounded-md p-2 text-center">
|
<div className="bg-muted/30 dark:bg-muted/20 rounded-md p-2 text-center">
|
||||||
<p className="text-xs text-muted-foreground">Area</p>
|
<p className="text-xs text-muted-foreground">Area</p>
|
||||||
<p className="font-medium">{farm.area}</p>
|
<p className="font-medium">{farm.TotalSize}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-muted/30 dark:bg-muted/20 rounded-md p-2 text-center">
|
<div className="bg-muted/30 dark:bg-muted/20 rounded-md p-2 text-center">
|
||||||
<p className="text-xs text-muted-foreground">Crops</p>
|
<p className="text-xs text-muted-foreground">Crops</p>
|
||||||
|
|||||||
@ -54,26 +54,38 @@ export default function FarmSetupPage() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// export interface Farm {
|
||||||
|
// CreatedAt: string;
|
||||||
|
// FarmType: string;
|
||||||
|
// Lat: number;
|
||||||
|
// Lon: number;
|
||||||
|
// Name: string;
|
||||||
|
// OwnerID: string;
|
||||||
|
// TotalSize: string;
|
||||||
|
// UUID: string;
|
||||||
|
// UpdatedAt: string;
|
||||||
|
// }
|
||||||
|
|
||||||
const filteredAndSortedFarms = (farms || [])
|
const filteredAndSortedFarms = (farms || [])
|
||||||
.filter(
|
.filter(
|
||||||
(farm) =>
|
(farm) =>
|
||||||
(activeFilter === "all" || farm.type === activeFilter) &&
|
(activeFilter === "all" || farm.FarmType === activeFilter) &&
|
||||||
(farm.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
(farm.Name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
farm.location.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
// farm.location.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
farm.type.toLowerCase().includes(searchQuery.toLowerCase()))
|
farm.FarmType.toLowerCase().includes(searchQuery.toLowerCase()))
|
||||||
)
|
)
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
if (sortOrder === "newest") {
|
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();
|
||||||
} else if (sortOrder === "oldest") {
|
} 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();
|
||||||
} else {
|
} else {
|
||||||
return a.name.localeCompare(b.name);
|
return a.Name.localeCompare(b.Name);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get distinct farm types.
|
// Get distinct farm types.
|
||||||
const farmTypes = ["all", ...new Set((farms || []).map((farm) => farm.type))];
|
const farmTypes = ["all", ...new Set((farms || []).map((farm) => farm.FarmType))];
|
||||||
|
|
||||||
const handleAddFarm = async (data: Partial<Farm>) => {
|
const handleAddFarm = async (data: Partial<Farm>) => {
|
||||||
await mutation.mutateAsync(data);
|
await mutation.mutateAsync(data);
|
||||||
@ -188,7 +200,7 @@ export default function FarmSetupPage() {
|
|||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-muted-foreground text-center max-w-md mb-6">
|
<p className="text-muted-foreground text-center max-w-md mb-6">
|
||||||
You haven't added any farms yet. Get started by adding your first farm.
|
You haven't added any farms yet. Get started by adding your first farm.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button
|
||||||
@ -226,13 +238,13 @@ export default function FarmSetupPage() {
|
|||||||
</motion.div>
|
</motion.div>
|
||||||
{filteredAndSortedFarms.map((farm, index) => (
|
{filteredAndSortedFarms.map((farm, index) => (
|
||||||
<motion.div
|
<motion.div
|
||||||
key={farm.id}
|
key={farm.UUID}
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
exit={{ opacity: 0, y: -20 }}
|
exit={{ opacity: 0, y: -20 }}
|
||||||
transition={{ duration: 0.2, delay: index * 0.05 }}
|
transition={{ duration: 0.2, delay: index * 0.05 }}
|
||||||
className="col-span-1">
|
className="col-span-1">
|
||||||
<FarmCard variant="farm" farm={farm} onClick={() => router.push(`/farms/${farm.id}`)} />
|
<FarmCard variant="farm" farm={farm} onClick={() => router.push(`/farms/${farm.UUID}`)} />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
))}
|
))}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import DynamicBreadcrumb from "./dynamic-breadcrumb";
|
|||||||
import { extractRoute } from "@/lib/utils";
|
import { extractRoute } from "@/lib/utils";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import { Toaster } from "@/components/ui/sonner";
|
import { Toaster } from "@/components/ui/sonner";
|
||||||
|
import { useForm, FormProvider } from "react-hook-form";
|
||||||
|
|
||||||
export default function AppLayout({
|
export default function AppLayout({
|
||||||
children,
|
children,
|
||||||
@ -16,21 +17,24 @@ export default function AppLayout({
|
|||||||
}>) {
|
}>) {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const currentPathname = extractRoute(pathname);
|
const currentPathname = extractRoute(pathname);
|
||||||
|
const form = useForm();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarProvider>
|
<SidebarProvider>
|
||||||
<AppSidebar />
|
<AppSidebar />
|
||||||
<SidebarInset>
|
<SidebarInset>
|
||||||
<header className="flex h-16 shrink-0 items-center gap-2">
|
<FormProvider {...form}>
|
||||||
<div className="flex items-center gap-2 px-4">
|
<header className="flex h-16 shrink-0 items-center gap-2">
|
||||||
<SidebarTrigger className="-ml-1" />
|
<div className="flex items-center gap-2 px-4">
|
||||||
<ThemeToggle />
|
<SidebarTrigger className="-ml-1" />
|
||||||
<Separator orientation="vertical" className="mr-2 h-4" />
|
<ThemeToggle />
|
||||||
<DynamicBreadcrumb pathname={currentPathname} />
|
<Separator orientation="vertical" className="mr-2 h-4" />
|
||||||
</div>
|
<DynamicBreadcrumb pathname={currentPathname} />
|
||||||
</header>
|
</div>
|
||||||
{children}
|
</header>
|
||||||
<Toaster />
|
{children}
|
||||||
|
<Toaster />
|
||||||
|
</FormProvider>
|
||||||
</SidebarInset>
|
</SidebarInset>
|
||||||
</SidebarProvider>
|
</SidebarProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -32,21 +32,33 @@ export interface CropAnalytics {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface Farm {
|
export interface Farm {
|
||||||
id: string;
|
CreatedAt: Date;
|
||||||
name: string;
|
FarmType: string;
|
||||||
location: string;
|
Lat: number;
|
||||||
type: string;
|
Lon: number;
|
||||||
createdAt: Date;
|
Name: string;
|
||||||
area?: string;
|
OwnerID: string;
|
||||||
crops: number;
|
TotalSize: string;
|
||||||
weather?: {
|
UUID: string;
|
||||||
temperature: number;
|
UpdatedAt: Date;
|
||||||
humidity: number;
|
|
||||||
rainfall: string;
|
|
||||||
sunlight: number;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// export interface Farm {
|
||||||
|
// id: string;
|
||||||
|
// name: string;
|
||||||
|
// location: string;
|
||||||
|
// type: string;
|
||||||
|
// createdAt: Date;
|
||||||
|
// area?: string;
|
||||||
|
// crops: number;
|
||||||
|
// weather?: {
|
||||||
|
// temperature: number;
|
||||||
|
// humidity: number;
|
||||||
|
// rainfall: string;
|
||||||
|
// sunlight: number;
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
|
||||||
export interface User {
|
export interface User {
|
||||||
ID: number;
|
ID: number;
|
||||||
UUID: string;
|
UUID: string;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user