mirror of
https://github.com/ForFarmTeam/ForFarm.git
synced 2025-12-18 21:44:08 +01:00
refactor: use camelCase instead of Pascal
This commit is contained in:
parent
9691b845d9
commit
1940a0034a
@ -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<Crop>) => Promise<void>;
|
||||
onSubmit: (data: Partial<Cropland>) => Promise<void>; // Expect Partial<Cropland>
|
||||
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<PlantResponse>({
|
||||
queryKey: ["plants"],
|
||||
queryFn: getPlants,
|
||||
staleTime: 1000 * 60 * 60, // Cache for 1 hour
|
||||
});
|
||||
|
||||
const form = useForm<z.infer<typeof cropFormSchema>>({
|
||||
resolver: zodResolver(cropFormSchema),
|
||||
defaultValues: {
|
||||
name: "",
|
||||
plantedDate: "",
|
||||
plantId: "", // Initialize plantId
|
||||
status: "planned",
|
||||
landSize: 0,
|
||||
growthStage: "Planned",
|
||||
priority: 1,
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = (values: z.infer<typeof cropFormSchema>) => {
|
||||
// Submit data shaped like Partial<Cropland>
|
||||
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 (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-6">
|
||||
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Crop Name</FormLabel>
|
||||
<FormLabel>Cropland Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Enter crop name" {...field} />
|
||||
<Input placeholder="e.g., North Field Tomatoes" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Plant Selection */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="plantedDate"
|
||||
name="plantId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Planted Date</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="date" {...field} />
|
||||
</FormControl>
|
||||
<FormLabel>Select Plant</FormLabel>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
disabled={isLoadingPlants || isErrorPlants}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue
|
||||
placeholder={
|
||||
isLoadingPlants
|
||||
? "Loading plants..."
|
||||
: isErrorPlants
|
||||
? "Error loading plants"
|
||||
: "Select the main plant for this cropland"
|
||||
}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{!isLoadingPlants &&
|
||||
!isErrorPlants &&
|
||||
plantData?.plants.map((plant) => (
|
||||
<SelectItem key={plant.uuid} value={plant.uuid}>
|
||||
{plant.name} {plant.variety ? `(${plant.variety})` : ""}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Status Selection */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="status"
|
||||
@ -79,6 +148,7 @@ export function AddCropForm({ onSubmit, onCancel }: AddCropFormProps) {
|
||||
<SelectItem value="planned">Planned</SelectItem>
|
||||
<SelectItem value="growing">Growing</SelectItem>
|
||||
<SelectItem value="harvested">Harvested</SelectItem>
|
||||
<SelectItem value="fallow">Fallow</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
@ -86,11 +156,73 @@ export function AddCropForm({ onSubmit, onCancel }: AddCropFormProps) {
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button type="button" variant="outline" onClick={onCancel}>
|
||||
{/* Land Size */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="landSize"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Land Size (e.g., Hectares)</FormLabel>
|
||||
<FormControl>
|
||||
{/* Use text input for flexibility, validation handles number conversion */}
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="e.g., 1.5"
|
||||
{...field}
|
||||
onChange={(e) => field.onChange(e.target.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Growth Stage */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="growthStage"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Initial Growth Stage</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="e.g., Seedling, Vegetative" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Priority */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="priority"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Priority</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="e.g., 1"
|
||||
{...field}
|
||||
onChange={(e) => field.onChange(e.target.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* TODO: Add GeoFeature input using the map component if needed within this dialog */}
|
||||
{/* <div className="h-64 border rounded-md overflow-hidden"> <GoogleMapWithDrawing onShapeDrawn={...} /> </div> */}
|
||||
|
||||
<div className="flex justify-end gap-2 pt-2">
|
||||
<Button type="button" variant="outline" onClick={onCancel} disabled={isSubmitting}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit">Add Crop</Button>
|
||||
<Button type="submit" disabled={isSubmitting || isLoadingPlants}>
|
||||
{isSubmitting ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : null}
|
||||
{isSubmitting ? "Adding Crop..." : "Add Crop"}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
@ -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<string, { bg: string; text: string; border: string }> = {
|
||||
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 (
|
||||
<Card
|
||||
onClick={onClick}
|
||||
className={`w-full h-full overflow-hidden transition-all duration-200 hover:shadow-lg border-muted/60 bg-white dark:bg-slate-800 hover:bg-muted/10 dark:hover:bg-slate-700`}>
|
||||
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`}>
|
||||
<CardHeader className="p-4 pb-0">
|
||||
<div className="flex items-center justify-between">
|
||||
<Badge variant="outline" className={`capitalize ${statusColor.bg} ${statusColor.text} ${statusColor.border}`}>
|
||||
{crop.status}
|
||||
{crop.status || "Unknown"}
|
||||
</Badge>
|
||||
<div className="flex items-center text-xs text-muted-foreground">
|
||||
<Calendar className="h-3 w-3 mr-1" />
|
||||
{crop.plantedDate.toLocaleDateString()}
|
||||
{displayDate}
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="p-4">
|
||||
<CardContent className="p-4 flex-grow">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className={`h-10 w-10 rounded-full ${statusColor.bg} flex-shrink-0 flex items-center justify-center`}>
|
||||
<Sprout className={`h-5 w-5 ${statusColor.text}`} />
|
||||
</div>
|
||||
{/* ... icon ... */}
|
||||
<div className="flex-1">
|
||||
<h3 className="text-xl font-medium mb-1">{crop.name}</h3>
|
||||
<h3 className="text-lg font-semibold mb-1 line-clamp-1">{crop.name}</h3> {/* Use camelCase name */}
|
||||
<p className="text-sm text-muted-foreground mb-2">
|
||||
{crop.variety} • {crop.area}
|
||||
{crop.growthStage || "N/A"} • {displayArea} {/* Use camelCase growthStage */}
|
||||
</p>
|
||||
|
||||
{crop.status !== "planned" && (
|
||||
<div className="space-y-2 mt-3">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-muted-foreground">Progress</span>
|
||||
<span className="font-medium">{crop.progress}%</span>
|
||||
</div>
|
||||
<Progress
|
||||
value={crop.progress}
|
||||
className={`h-2 ${
|
||||
crop.status === "growing" ? "bg-green-500" : crop.status === "harvested" ? "bg-yellow-500" : ""
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{crop.status === "growing" && (
|
||||
{crop.growthStage && (
|
||||
<div className="flex items-center mt-3 text-sm">
|
||||
<div className="flex items-center gap-1 text-green-600 dark:text-green-300">
|
||||
<BarChart className="h-3.5 w-3.5" />
|
||||
<span className="font-medium">Health: {crop.healthScore}%</span>
|
||||
<div className="flex items-center gap-1 text-muted-foreground">
|
||||
<Layers className="h-3.5 w-3.5" />
|
||||
{/* Use camelCase growthStage */}
|
||||
<span className="font-medium">Stage: {crop.growthStage}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter className="p-4 pt-0">
|
||||
<CardFooter className="p-4 pt-0 mt-auto">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="ml-auto gap-1 text-green-600 dark:text-green-300 hover:text-green-700 dark:hover:text-green-400 hover:bg-green-50/50 dark:hover:bg-green-800">
|
||||
className="ml-auto gap-1 text-primary hover:text-primary/80 hover:bg-primary/10">
|
||||
View details <ArrowRight className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</CardFooter>
|
||||
|
||||
@ -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<Crop>) => Promise<void>;
|
||||
onSubmit: (data: Partial<Cropland>) => Promise<void>;
|
||||
isSubmitting: boolean;
|
||||
}
|
||||
|
||||
export function CropDialog({ open, onOpenChange, onSubmit }: CropDialogProps) {
|
||||
const [selectedPlant, setSelectedPlant] = useState<string | null>(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<string | null>(null);
|
||||
// State to hold the structured GeoFeature data
|
||||
const [geoFeature, setGeoFeature] = useState<GeoFeatureData | null>(null);
|
||||
const [calculatedArea, setCalculatedArea] = useState<number | null>(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<PlantResponse>({
|
||||
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<Cropland> = {
|
||||
// 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 (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<VisuallyHidden>
|
||||
<DialogTitle></DialogTitle>
|
||||
</VisuallyHidden>
|
||||
<DialogContent className="sm:max-w-[900px] p-0">
|
||||
<div className="grid md:grid-cols-2 h-[600px]">
|
||||
{/* Left side - Plant Selection */}
|
||||
<div className="p-6 overflow-y-auto border-r dark:border-slate-700">
|
||||
<h2 className="text-lg font-semibold mb-4">Select Plant to Grow</h2>
|
||||
<div className="space-y-4">
|
||||
{plants.map((plant) => (
|
||||
<Card
|
||||
key={plant.id}
|
||||
className={cn(
|
||||
"p-4 cursor-pointer hover:bg-muted/50 dark:hover:bg-muted/40 transition-colors",
|
||||
selectedPlant === plant.id && "border-primary dark:border-primary dark:bg-primary/5 bg-primary/5"
|
||||
)}
|
||||
onClick={() => setSelectedPlant(plant.id)}>
|
||||
<div className="flex items-center gap-4">
|
||||
<img
|
||||
src={plant.image || "/placeholder.svg"}
|
||||
alt={plant.name}
|
||||
className="w-20 h-20 rounded-lg object-cover"
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="font-medium">{plant.name}</h3>
|
||||
{selectedPlant === plant.id && <Check className="h-4 w-4 text-primary" />}
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">Growth time: {plant.growthTime}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<DialogContent className="sm:max-w-[950px] md:max-w-[1100px] lg:max-w-[1200px] xl:max-w-7xl p-0 max-h-[90vh] flex flex-col">
|
||||
<DialogHeader className="p-6 pb-0">
|
||||
<DialogTitle className="text-xl font-semibold">Create New Cropland</DialogTitle>
|
||||
<DialogDescription>
|
||||
Select a plant and draw the cropland boundary or mark its location on the map.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{/* Right side - Map */}
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 bg-muted/10 dark:bg-muted/20">
|
||||
<div className="h-full w-full flex items-center justify-center">
|
||||
<GoogleMapWithDrawing />
|
||||
<div className="flex-grow grid md:grid-cols-12 gap-0 overflow-hidden">
|
||||
{/* Left Side: Plant Selection */}
|
||||
<div className="md:col-span-4 lg:col-span-3 p-6 pt-2 border-r dark:border-slate-700 overflow-y-auto">
|
||||
<h3 className="text-md font-medium mb-4 sticky top-0 bg-background py-2">1. Select Plant</h3>
|
||||
{/* Plant selection UI */}
|
||||
{isLoadingPlants && (
|
||||
<div className="flex items-center justify-center py-10">
|
||||
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
|
||||
<span className="ml-2 text-muted-foreground">Loading plants...</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{isErrorPlants && (
|
||||
<div className="text-destructive flex items-center gap-2 bg-destructive/10 p-3 rounded-md">
|
||||
<AlertTriangle className="h-5 w-5" />
|
||||
<span>Error loading plants: {(errorPlants as Error)?.message}</span>
|
||||
</div>
|
||||
)}
|
||||
{!isLoadingPlants && !isErrorPlants && plants.length === 0 && (
|
||||
<div className="text-center py-10 text-muted-foreground">No plants available.</div>
|
||||
)}
|
||||
{!isLoadingPlants && !isErrorPlants && plants.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
{plants.map((plant) => (
|
||||
<Card
|
||||
key={plant.uuid}
|
||||
className={cn(
|
||||
"p-3 cursor-pointer hover:bg-muted/50 dark:hover:bg-muted/40 transition-colors",
|
||||
selectedPlantUUID === plant.uuid &&
|
||||
"border-2 border-primary dark:border-primary dark:bg-primary/5 bg-primary/5"
|
||||
)}
|
||||
onClick={() => setSelectedPlantUUID(plant.uuid)}>
|
||||
<CardContent className="p-0">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="w-16 h-16 rounded-md bg-gradient-to-br from-green-100 to-lime-100 dark:from-green-900 dark:to-lime-900 flex items-center justify-center">
|
||||
<Sprout className="w-8 h-8 text-green-600 dark:text-green-400" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="font-medium text-sm">
|
||||
{plant.name} <span className="text-xs text-muted-foreground">({plant.variety})</span>
|
||||
</h4>
|
||||
{selectedPlantUUID === plant.uuid && (
|
||||
<Check className="h-4 w-4 text-primary flex-shrink-0" />
|
||||
)}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground mt-1 space-y-0.5">
|
||||
<p className="flex items-center">
|
||||
<CalendarDays className="h-3 w-3 mr-1" /> Maturity: ~{plant.daysToMaturity ?? "N/A"} days
|
||||
</p>
|
||||
<p className="flex items-center">
|
||||
<Thermometer className="h-3 w-3 mr-1" /> Temp: {plant.optimalTemp ?? "N/A"}°C
|
||||
</p>
|
||||
<p className="flex items-center">
|
||||
<Droplets className="h-3 w-3 mr-1" /> Water: {plant.waterNeeds ?? "N/A"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="absolute bottom-0 left-0 right-0 p-4 bg-background dark:bg-background border-t dark:border-slate-700">
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleSubmit} disabled={!selectedPlant}>
|
||||
Plant Crop
|
||||
</Button>
|
||||
{/* Right Side: Map */}
|
||||
<div className="md:col-span-8 lg:col-span-9 p-6 pt-2 flex flex-col overflow-hidden">
|
||||
<h3 className="text-md font-medium mb-4">2. Define Boundary / Location</h3>
|
||||
<div className="flex-grow bg-muted/30 dark:bg-muted/20 rounded-md border dark:border-slate-700 overflow-hidden relative">
|
||||
<GoogleMapWithDrawing onShapeDrawn={handleShapeDrawn} />
|
||||
|
||||
{/* Display feedback based on drawn shape */}
|
||||
{geoFeature?.type === "polygon" && calculatedArea !== null && (
|
||||
<div className="absolute bottom-2 right-2 bg-background/80 backdrop-blur-sm p-2 rounded shadow-md text-sm flex items-center gap-1">
|
||||
<Maximize className="h-3 w-3 text-blue-600" />
|
||||
Area: {calculatedArea.toFixed(2)} m²
|
||||
</div>
|
||||
)}
|
||||
{geoFeature?.type === "polyline" && geoFeature.path && (
|
||||
<div className="absolute bottom-2 right-2 bg-background/80 backdrop-blur-sm p-2 rounded shadow-md text-sm flex items-center gap-1">
|
||||
<MapPin className="h-3 w-3 text-orange-600" />
|
||||
Boundary path defined ({geoFeature.path.length} points).
|
||||
</div>
|
||||
)}
|
||||
{geoFeature?.type === "marker" && geoFeature.position && (
|
||||
<div className="absolute bottom-2 right-2 bg-background/80 backdrop-blur-sm p-2 rounded shadow-md text-sm flex items-center gap-1">
|
||||
<MapPin className="h-3 w-3 text-red-600" />
|
||||
Marker set at {geoFeature.position.lat.toFixed(4)}, {geoFeature.position.lng.toFixed(4)}.
|
||||
</div>
|
||||
)}
|
||||
{!geometryLib && (
|
||||
<div className="absolute inset-0 bg-background/50 flex items-center justify-center text-muted-foreground">
|
||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" /> Loading map tools...
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-2">
|
||||
Use the drawing tools (Polygon <Maximize className="inline h-3 w-3" />, Polyline{" "}
|
||||
<MapPin className="inline h-3 w-3" />, Marker <MapPin className="inline h-3 w-3 text-red-500" />) above
|
||||
the map. Area is calculated for polygons.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Dialog Footer */}
|
||||
<DialogFooter className="p-6 pt-4 border-t dark:border-slate-700 mt-auto">
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)} disabled={isSubmitting}>
|
||||
Cancel
|
||||
</Button>
|
||||
{/* Disable submit if no plant OR no feature is selected */}
|
||||
<Button onClick={handleSubmit} disabled={!selectedPlantUUID || !geoFeature || isSubmitting}>
|
||||
{isSubmitting ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Creating...
|
||||
</>
|
||||
) : (
|
||||
"Create Cropland"
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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<CropDetailPageParams>;
|
||||
}) {
|
||||
export default function CropDetailPage() {
|
||||
const router = useRouter();
|
||||
const [crop, setCrop] = useState<Crop | null>(null);
|
||||
const [analytics, setAnalytics] = useState<CropAnalytics | null>(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<Farm>({
|
||||
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<Cropland>({
|
||||
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<PlantResponse>({
|
||||
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<CropAnalytics | null>({
|
||||
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 (
|
||||
<div className="min-h-screen flex items-center justify-center bg-background text-foreground">
|
||||
Loading...
|
||||
<Loader2 className="h-8 w-8 animate-spin text-green-600" />
|
||||
<span className="ml-2">Loading crop details...</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// --- Error State ---
|
||||
if (isError || !cropland) {
|
||||
console.error("Error loading crop details:", error);
|
||||
return (
|
||||
<div className="min-h-screen container max-w-7xl p-6 mx-auto">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-fit gap-2 text-muted-foreground mb-6"
|
||||
onClick={() => router.back()}>
|
||||
<ArrowLeft className="h-4 w-4" /> Back
|
||||
</Button>
|
||||
<Alert variant="destructive">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<AlertTitle>Error Loading Crop Details</AlertTitle>
|
||||
<AlertDescription>
|
||||
{isErrorCropland
|
||||
? `Crop with ID ${cropId} not found or could not be loaded.`
|
||||
: (error as Error)?.message || "An unexpected error occurred."}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// --- 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 (
|
||||
<div className="min-h-screen bg-background text-foreground">
|
||||
<div className="container max-w-7xl p-6 mx-auto">
|
||||
{/* Breadcrumbs */}
|
||||
<nav className="flex items-center text-sm text-muted-foreground mb-4">
|
||||
<Button
|
||||
variant="link"
|
||||
className="p-0 h-auto font-normal text-muted-foreground hover:text-primary"
|
||||
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 hover:text-primary"
|
||||
onClick={() => router.push("/farms")}>
|
||||
Farms
|
||||
</Button>
|
||||
<ChevronRight className="h-3.5 w-3.5 mx-1" />
|
||||
<Button
|
||||
variant="link"
|
||||
className="p-0 h-auto font-normal text-muted-foreground hover:text-primary max-w-[150px] truncate"
|
||||
onClick={() => router.push(`/farms/${farmId}`)}>
|
||||
{farm?.name || "Farm"} {/* Use camelCase */}
|
||||
</Button>
|
||||
<ChevronRight className="h-3.5 w-3.5 mx-1" />
|
||||
<span className="text-foreground font-medium truncate">{cropland.name || "Crop"}</span> {/* Use camelCase */}
|
||||
</nav>
|
||||
|
||||
{/* Header */}
|
||||
<div className="flex flex-col gap-6 mb-8">
|
||||
<div className="flex items-center justify-between">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="gap-2 text-green-700 dark:text-green-300 hover:text-green-800 dark:hover:text-green-200 hover:bg-green-100/50 dark:hover:bg-green-800/50"
|
||||
onClick={() => router.back()}
|
||||
>
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-fit gap-2 text-muted-foreground"
|
||||
onClick={() => router.push(`/farms/${farmId}`)}>
|
||||
<ArrowLeft className="h-4 w-4" /> Back to Farm
|
||||
</Button>
|
||||
<HoverCard>
|
||||
<HoverCardTrigger asChild>
|
||||
<Button variant="outline" className="gap-2">
|
||||
<Calendar className="h-4 w-4" /> Timeline
|
||||
</Button>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent className="w-80">
|
||||
<div className="flex justify-between space-x-4">
|
||||
<Avatar>
|
||||
<AvatarImage src="/placeholder.svg" />
|
||||
<AvatarFallback>
|
||||
<Sprout className="h-4 w-4" />
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="space-y-1">
|
||||
<h4 className="text-sm font-semibold">Growth Timeline</h4>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Planted on {crop.plantedDate.toLocaleDateString()}
|
||||
</p>
|
||||
<div className="flex items-center pt-2">
|
||||
<Separator className="w-full" />
|
||||
<span className="mx-2 text-xs text-muted-foreground">
|
||||
{Math.floor(analytics.growthProgress)}% Complete
|
||||
</span>
|
||||
<Separator className="w-full" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
{/* Hover Card (removed for simplicity, add back if needed) */}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col md:flex-row justify-between gap-4">
|
||||
<div className="space-y-1">
|
||||
<h1 className="text-3xl font-bold tracking-tight">{crop.name}</h1>
|
||||
<h1 className="text-3xl font-bold tracking-tight">{cropland.name}</h1> {/* Use camelCase */}
|
||||
<p className="text-muted-foreground">
|
||||
{crop.variety} • {crop.area}
|
||||
{plant?.variety || "Unknown Variety"} • {displayArea} {/* Use camelCase */}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex flex-col items-end">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={`${healthColors[analytics.plantHealth]} border`}
|
||||
>
|
||||
Health Score: {crop.healthScore}%
|
||||
</Badge>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="bg-blue-50 dark:bg-blue-900 text-blue-600 dark:text-blue-300"
|
||||
>
|
||||
Growing
|
||||
<Badge variant="outline" className={`${healthColors[healthStatus]} border capitalize`}>
|
||||
{cropland.status} {/* Use camelCase */}
|
||||
</Badge>
|
||||
</div>
|
||||
{crop.expectedHarvest ? (
|
||||
{expectedHarvestDate ? (
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
Expected harvest:{" "}
|
||||
{crop.expectedHarvest.toLocaleDateString()}
|
||||
Expected harvest: {expectedHarvestDate.toLocaleDateString()}
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
Expected harvest date not available
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground mt-1">Expected harvest date not available</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@ -208,138 +265,123 @@ export default function CropDetailPage({
|
||||
<Button
|
||||
key={action.title}
|
||||
variant="outline"
|
||||
className={`h-auto p-4 flex flex-col items-center gap-3 transition-all group ${action.color} hover:scale-105`}
|
||||
onClick={action.onClick}
|
||||
>
|
||||
className={`h-auto p-4 flex flex-col items-center gap-3 transition-all group ${action.color} hover:scale-105 border-border/30`}
|
||||
onClick={action.onClick}>
|
||||
<div
|
||||
className={`p-3 rounded-lg ${action.color} group-hover:scale-110 transition-transform`}
|
||||
>
|
||||
className={`p-3 rounded-lg ${action.color.replace(
|
||||
"text-",
|
||||
"bg-"
|
||||
)}/20 group-hover:scale-110 transition-transform`}>
|
||||
<action.icon className="h-5 w-5" />
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="font-medium mb-1">{action.title}</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{action.description}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">{action.description}</p>
|
||||
</div>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Environmental Metrics */}
|
||||
<Card className="border-green-100 dark:border-green-700">
|
||||
<Card className="border-border/30">
|
||||
<CardHeader>
|
||||
<CardTitle>Environmental Conditions</CardTitle>
|
||||
<CardDescription>
|
||||
Real-time monitoring of growing conditions
|
||||
</CardDescription>
|
||||
<CardDescription>Real-time monitoring data</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid gap-6">
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||
{[
|
||||
{
|
||||
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) => (
|
||||
<Card
|
||||
key={metric.label}
|
||||
className="border-none shadow-none bg-gradient-to-br from-white to-gray-50/50 dark:from-slate-800 dark:to-slate-700/50"
|
||||
>
|
||||
<Card key={metric.label} className="border-border/30 shadow-none bg-card dark:bg-slate-800">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className={`p-2 rounded-lg ${metric.bg}`}>
|
||||
<metric.icon
|
||||
className={`h-4 w-4 ${metric.color}`}
|
||||
/>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className={`p-2 rounded-lg ${metric.bg}/50`}>
|
||||
<metric.icon className={`h-4 w-4 ${metric.color}`} />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-muted-foreground">
|
||||
{metric.label}
|
||||
</p>
|
||||
<p className="text-2xl font-semibold tracking-tight">
|
||||
{metric.value}
|
||||
</p>
|
||||
<p className="text-sm font-medium text-muted-foreground">{metric.label}</p>
|
||||
<p className="text-xl font-semibold tracking-tight">{metric.value}</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Growth Progress */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="font-medium">Growth Progress</span>
|
||||
<span className="text-muted-foreground">
|
||||
{analytics.growthProgress}%
|
||||
</span>
|
||||
<span className="text-muted-foreground">{growthProgress}%</span>
|
||||
</div>
|
||||
<Progress
|
||||
value={analytics.growthProgress}
|
||||
className="h-2"
|
||||
/>
|
||||
<Progress value={growthProgress} className="h-2" />
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Based on {daysToMaturity ? `${daysToMaturity} days` : "N/A"} to maturity.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Next Action Card */}
|
||||
<Card className="border-green-100 dark:border-green-700 bg-green-50/50 dark:bg-green-900/50">
|
||||
<Card className="border-blue-100 dark:border-blue-700 bg-blue-50/50 dark:bg-blue-900/50">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="p-2 rounded-lg bg-green-100 dark:bg-green-800">
|
||||
<Timer className="h-4 w-4 text-green-600 dark:text-green-300" />
|
||||
<div className="p-2 rounded-lg bg-blue-100 dark:bg-blue-800">
|
||||
<Timer className="h-4 w-4 text-blue-600 dark:text-blue-300" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium mb-1">
|
||||
Next Action Required
|
||||
</p>
|
||||
<p className="font-medium mb-1">Next Action Required</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{analytics.nextAction}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Due by{" "}
|
||||
{analytics.nextActionDue.toLocaleDateString()}
|
||||
{analytics?.nextAction || "Check crop status"}
|
||||
</p>
|
||||
{analytics?.nextActionDue && (
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Due by {new Date(analytics.nextActionDue).toLocaleDateString()}
|
||||
</p>
|
||||
)}
|
||||
{!analytics?.nextAction && (
|
||||
<p className="text-xs text-muted-foreground mt-1">No immediate actions required.</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
@ -349,13 +391,18 @@ export default function CropDetailPage({
|
||||
</Card>
|
||||
|
||||
{/* Map Section */}
|
||||
<Card className="border-green-100 dark:border-green-700">
|
||||
<Card className="border-border/30">
|
||||
<CardHeader>
|
||||
<CardTitle>Field Map</CardTitle>
|
||||
<CardDescription>View and manage crop location</CardDescription>
|
||||
<CardTitle>Cropland Location / Boundary</CardTitle>
|
||||
<CardDescription>Visual representation on the farm</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="p-0 h-[400px]">
|
||||
<GoogleMapWithDrawing />
|
||||
<CardContent className="p-0 h-[400px] overflow-hidden rounded-b-lg">
|
||||
{/* TODO: Ensure GoogleMapWithDrawing correctly parses and displays GeoFeatureData */}
|
||||
<GoogleMapWithDrawing
|
||||
initialFeatures={cropland.geoFeature ? [cropland.geoFeature] : undefined}
|
||||
drawingMode={null}
|
||||
editable={false}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
@ -363,83 +410,64 @@ export default function CropDetailPage({
|
||||
{/* Right Column */}
|
||||
<div className="md:col-span-4 space-y-6">
|
||||
{/* Nutrient Levels */}
|
||||
<Card className="border-green-100 dark:border-green-700">
|
||||
<Card className="border-border/30">
|
||||
<CardHeader>
|
||||
<CardTitle>Nutrient Levels</CardTitle>
|
||||
<CardDescription>Current soil composition</CardDescription>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<LeafIcon className="h-5 w-5 text-amber-600 dark:text-amber-400" />
|
||||
Nutrient Levels
|
||||
</CardTitle>
|
||||
<CardDescription>Soil composition (if available)</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{[
|
||||
{
|
||||
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) => (
|
||||
<div key={nutrient.name} className="space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="font-medium">{nutrient.name}</span>
|
||||
<span className="text-muted-foreground">
|
||||
{nutrient.value}%
|
||||
</span>
|
||||
<span className="text-muted-foreground">{nutrient.value ?? "N/A"}%</span>
|
||||
</div>
|
||||
<Progress
|
||||
value={nutrient.value}
|
||||
className={`h-2 ${nutrient.color}`}
|
||||
value={nutrient.value ?? 0}
|
||||
className={`h-2 ${
|
||||
nutrient.value !== null && nutrient.value !== undefined ? nutrient.color : "bg-muted"
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
{!analytics?.nutrientLevels && (
|
||||
<p className="text-center text-sm text-muted-foreground py-4">Nutrient data not available.</p>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Recent Activity */}
|
||||
<Card className="border-green-100 dark:border-green-700">
|
||||
<Card className="border-border/30">
|
||||
<CardHeader>
|
||||
<CardTitle>Recent Activity</CardTitle>
|
||||
<CardDescription>Latest updates and changes</CardDescription>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<History className="h-5 w-5 text-purple-600 dark:text-purple-400" />
|
||||
Recent Activity
|
||||
</CardTitle>
|
||||
<CardDescription>Latest updates (placeholder)</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ScrollArea className="h-[300px] pr-4">
|
||||
{[...Array(5)].map((_, i) => (
|
||||
<div key={i} className="mb-4 last:mb-0">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="p-2 rounded-lg bg-gray-100 dark:bg-gray-800">
|
||||
<Activity icon={i} />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium">
|
||||
{
|
||||
[
|
||||
"Irrigation completed",
|
||||
"Nutrient levels checked",
|
||||
"Growth measurement taken",
|
||||
"Pest inspection completed",
|
||||
"Soil pH tested",
|
||||
][i]
|
||||
}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
2 hours ago
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{i < 4 && (
|
||||
<Separator className="my-4 dark:bg-slate-700" />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<div className="text-center py-10 text-muted-foreground">No recent activity logged.</div>
|
||||
</ScrollArea>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@ -447,38 +475,30 @@ export default function CropDetailPage({
|
||||
</div>
|
||||
|
||||
{/* Dialogs */}
|
||||
<ChatbotDialog
|
||||
open={isChatOpen}
|
||||
onOpenChange={setIsChatOpen}
|
||||
cropName={crop.name}
|
||||
/>
|
||||
<AnalyticsDialog
|
||||
open={isAnalyticsOpen}
|
||||
onOpenChange={setIsAnalyticsOpen}
|
||||
crop={crop}
|
||||
analytics={analytics}
|
||||
/>
|
||||
<ChatbotDialog open={isChatOpen} onOpenChange={setIsChatOpen} cropName={cropland.name || "this crop"} />
|
||||
{/* Ensure AnalyticsDialog uses the correct props */}
|
||||
{analytics && (
|
||||
<AnalyticsDialog
|
||||
open={isAnalyticsOpen}
|
||||
onOpenChange={setIsAnalyticsOpen}
|
||||
// The dialog expects a `Crop` type, but we have `Cropland` and `CropAnalytics`
|
||||
// We need to construct a simplified `Crop` object or update the dialog prop type
|
||||
crop={{
|
||||
// Constructing a simplified Crop object
|
||||
uuid: cropland.uuid,
|
||||
farmId: cropland.farmId,
|
||||
name: cropland.name,
|
||||
createdAt: cropland.createdAt, // Use createdAt as plantedDate
|
||||
status: cropland.status,
|
||||
variety: plant?.variety, // Get from plant data
|
||||
area: `${cropland.landSize} ha`, // Convert landSize
|
||||
progress: growthProgress, // Use calculated/fetched progress
|
||||
// healthScore might map to plantHealth
|
||||
}}
|
||||
analytics={analytics} // Pass fetched analytics
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper component to render an activity icon based on the index.
|
||||
*/
|
||||
function Activity({ icon }: { icon: number }) {
|
||||
const icons = [
|
||||
<Droplets key="0" className="h-4 w-4 text-blue-500 dark:text-blue-300" />,
|
||||
<Leaf key="1" className="h-4 w-4 text-green-500 dark:text-green-300" />,
|
||||
<LineChart
|
||||
key="2"
|
||||
className="h-4 w-4 text-purple-500 dark:text-purple-300"
|
||||
/>,
|
||||
<Sprout key="3" className="h-4 w-4 text-yellow-500 dark:text-yellow-300" />,
|
||||
<ThermometerSun
|
||||
key="4"
|
||||
className="h-4 w-4 text-orange-500 dark:text-orange-300"
|
||||
/>,
|
||||
];
|
||||
return icons[icon];
|
||||
}
|
||||
|
||||
@ -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<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");
|
||||
// ---------------------------------------------------------------
|
||||
// Local State for dialog and crop filter management
|
||||
// ---------------------------------------------------------------
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
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);
|
||||
// }
|
||||
// }
|
||||
// ---------------------------------------------------------------
|
||||
// Data fetching: Farm details and Crops using React Query.
|
||||
// - See: https://tanstack.com/query
|
||||
// ---------------------------------------------------------------
|
||||
const {
|
||||
data: farm,
|
||||
isLoading: isLoadingFarm,
|
||||
isError: isErrorFarm,
|
||||
error: errorFarm,
|
||||
} = useQuery<Farm>({
|
||||
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<CropResponse>({
|
||||
// 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<Crop>) => {
|
||||
// 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<Cropland>) => 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<Cropland>) => {
|
||||
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<string, number>
|
||||
);
|
||||
}, [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 (
|
||||
// <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>
|
||||
// ===================================================================
|
||||
// Render: Main page layout segmented into breadcrumbs, farm cards,
|
||||
// crop management, and the crop add dialog.
|
||||
// ===================================================================
|
||||
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 Navigation Section
|
||||
------------------------------ */}
|
||||
<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>
|
||||
{/* ------------------------------
|
||||
Back Navigation 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>
|
||||
// )}
|
||||
{/* ------------------------------
|
||||
Error and Loading States
|
||||
------------------------------ */}
|
||||
{isError && !isLoadingFarm && !farm && (
|
||||
<Alert variant="destructive">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<AlertTitle>Error Loading Farm</AlertTitle>
|
||||
<AlertDescription>{(error as Error)?.message || "Could not load farm details."}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
{isErrorCrops && (
|
||||
<Alert variant="destructive" className="mt-4">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<AlertTitle>Error Loading Crops</AlertTitle>
|
||||
<AlertDescription>{(errorCrops as Error)?.message || "Could not load crop data."}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
{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>
|
||||
)}
|
||||
|
||||
// {/* 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 and Statistics
|
||||
------------------------------ */}
|
||||
{!isLoadingFarm && !isErrorFarm && 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.farmType}
|
||||
</Badge>
|
||||
<div className="flex items-center text-sm text-muted-foreground">
|
||||
<Calendar className="h-4 w-4 mr-1" />
|
||||
Created {new Date(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" />
|
||||
Lat: {farm.lat?.toFixed(4)}, Lon: {farm.lon?.toFixed(4)}
|
||||
</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.totalSize}</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">{isLoadingCrops ? "..." : cropCounts.all ?? 0}</p>
|
||||
</div>
|
||||
<div className="bg-muted/30 dark:bg-muted/20 rounded-lg p-3">
|
||||
<p className="text-xs text-muted-foreground">Growing</p>
|
||||
<p className="text-lg font-semibold">{isLoadingCrops ? "..." : cropCounts.growing ?? 0}</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">{isLoadingCrops ? "..." : cropCounts.harvested ?? 0}</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
// {/* 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 Overview Card */}
|
||||
<Card className="md:col-span-4">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg font-semibold flex items-center">
|
||||
<Sun className="h-5 w-5 mr-2 text-yellow-500" /> Weather Overview
|
||||
</CardTitle>
|
||||
<CardDescription>Current conditions</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-muted-foreground">Temperature</span>
|
||||
<span className="font-medium">25°C</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-muted-foreground">Humidity</span>
|
||||
<span className="font-medium">60%</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-muted-foreground">Wind</span>
|
||||
<span className="font-medium">10 km/h</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-muted-foreground">Rainfall (24h)</span>
|
||||
<span className="font-medium">2 mm</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
// {/* 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: List and Filtering Tabs
|
||||
------------------------------ */}
|
||||
<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 / Croplands
|
||||
</h2>
|
||||
<p className="text-sm text-muted-foreground">Manage and monitor all croplands in this farm</p>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => setIsDialogOpen(true)}
|
||||
className="gap-2 bg-green-600 hover:bg-green-700 w-full sm:w-auto"
|
||||
disabled={mutation.isPending}>
|
||||
{mutation.isPending ? <Loader2 className="h-4 w-4 animate-spin" /> : <Plus className="h-4 w-4" />}
|
||||
Add New Crop
|
||||
</Button>
|
||||
</div>
|
||||
{mutation.isError && (
|
||||
<Alert variant="destructive" className="mb-4">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<AlertTitle>Failed to Add Crop</AlertTitle>
|
||||
<AlertDescription>
|
||||
{(mutation.error as Error)?.message || "Could not add the crop. Please try again."}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
// {/* 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 value={activeFilter} onValueChange={setActiveFilter} className="mt-6">
|
||||
<TabsList>
|
||||
{availableStatuses.map((status) => (
|
||||
<TabsTrigger key={status} value={status} className="capitalize">
|
||||
{status === "all" ? "All" : status} ({isLoadingCrops ? "..." : cropCounts[status] ?? 0})
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
|
||||
// <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>
|
||||
{isLoadingCrops ? (
|
||||
<div className="flex justify-center py-12">
|
||||
<Loader2 className="h-6 w-6 text-green-600 animate-spin" />
|
||||
</div>
|
||||
) : isErrorCrops ? (
|
||||
<div className="text-center py-12 text-destructive">Failed to load crops.</div>
|
||||
) : (
|
||||
availableStatuses.map((status) => (
|
||||
<TabsContent key={status} value={status} className="mt-6">
|
||||
{filteredCrops.length === 0 && activeFilter === status ? (
|
||||
<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 {status === "all" ? "" : status} crops found
|
||||
</h3>
|
||||
<p className="text-muted-foreground text-center max-w-md mb-6">
|
||||
{status === "all"
|
||||
? "You haven't added any crops to this farm yet."
|
||||
: `No crops with status "${status}" found.`}
|
||||
</p>
|
||||
<Button onClick={() => setIsDialogOpen(true)} className="gap-2">
|
||||
<Plus className="h-4 w-4" />
|
||||
Add {status === "all" ? "your first" : "a new"} crop
|
||||
</Button>
|
||||
</div>
|
||||
) : activeFilter === status && filteredCrops.length > 0 ? (
|
||||
<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.uuid}
|
||||
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/${farmId}/crops/${crop.uuid}`)}
|
||||
/>
|
||||
</motion.div>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
) : null}
|
||||
</TabsContent>
|
||||
))
|
||||
)}
|
||||
</Tabs>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
// <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) {
|
||||
return <div>hello</div>;
|
||||
{/* ------------------------------
|
||||
Add Crop Dialog Component
|
||||
- Passes the mutation state to display loading indicators.
|
||||
------------------------------ */}
|
||||
<CropDialog
|
||||
open={isDialogOpen}
|
||||
onOpenChange={setIsDialogOpen}
|
||||
onSubmit={handleAddCrop}
|
||||
isSubmitting={mutation.isPending}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -71,11 +71,11 @@ export function AddFarmForm({ onSubmit, onCancel }: AddFarmFormProps) {
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
const farmData: Partial<Farm> = {
|
||||
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();
|
||||
|
||||
@ -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 (
|
||||
<Card className={cardClasses} onClick={onClick}>
|
||||
@ -49,7 +49,7 @@ export function FarmCard({ variant, farm, onClick }: FarmCardProps) {
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="capitalize bg-green-50 dark:bg-green-900 text-green-700 dark:text-green-300 border-green-200">
|
||||
{farm.FarmType}
|
||||
{farm.farmType}
|
||||
</Badge>
|
||||
<div className="flex items-center text-xs text-muted-foreground">
|
||||
<CalendarDays className="h-3 w-3 mr-1" />
|
||||
@ -63,19 +63,19 @@ export function FarmCard({ variant, farm, onClick }: FarmCardProps) {
|
||||
<Sprout className="h-5 w-5 text-green-600" />
|
||||
</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">
|
||||
<MapPin className="h-3.5 w-3.5 mr-1 flex-shrink-0" />
|
||||
<span className="truncate">{farm.Lat}</span>
|
||||
<span className="truncate">{farm.lat}</span>
|
||||
</div>
|
||||
<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">
|
||||
<p className="text-xs text-muted-foreground">Area</p>
|
||||
<p className="font-medium">{farm.TotalSize}</p>
|
||||
<p className="font-medium">{farm.totalSize}</p>
|
||||
</div>
|
||||
<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="font-medium">{farm.Crops ? farm.Crops.length : 0}</p>
|
||||
<p className="font-medium">{farm.crops ? farm.crops.length : 0}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -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<Farm[]>({
|
||||
// Use Farm[] type
|
||||
queryKey: ["farms"],
|
||||
queryFn: fetchFarms,
|
||||
staleTime: 60 * 1000,
|
||||
});
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: (data: Partial<Farm>) => createFarm(data),
|
||||
// Pass the correct type to createFarm
|
||||
mutationFn: (data: Partial<Omit<Farm, "uuid" | "createdAt" | "updatedAt" | "crops" | "ownerId">>) =>
|
||||
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<Farm>) => {
|
||||
await mutation.mutateAsync(data);
|
||||
@ -133,6 +136,7 @@ export default function FarmSetupPage() {
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
{/* DropdownMenu remains the same, Check icon was missing */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" className="gap-2">
|
||||
@ -228,23 +232,17 @@ export default function FarmSetupPage() {
|
||||
{!isLoading && !isError && filteredAndSortedFarms.length > 0 && (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
||||
<AnimatePresence>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="col-span-1">
|
||||
<motion.div /* ... */>
|
||||
<FarmCard variant="add" onClick={() => setIsDialogOpen(true)} />
|
||||
</motion.div>
|
||||
{filteredAndSortedFarms.map((farm, index) => (
|
||||
<motion.div
|
||||
key={farm.UUID}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
key={farm.uuid} // Use camelCase uuid initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
transition={{ duration: 0.2, delay: index * 0.05 }}
|
||||
className="col-span-1">
|
||||
<FarmCard variant="farm" farm={farm} onClick={() => router.push(`/farms/${farm.UUID}`)} />
|
||||
<FarmCard variant="farm" farm={farm} onClick={() => router.push(`/farms/${farm.uuid}`)} />
|
||||
</motion.div>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
@ -260,6 +258,7 @@ export default function FarmSetupPage() {
|
||||
<DialogTitle>Add New Farm</DialogTitle>
|
||||
<DialogDescription>Fill out the details below to add a new farm to your account.</DialogDescription>
|
||||
</DialogHeader>
|
||||
{/* Pass handleAddFarm (which now expects Partial<Farm>) */}
|
||||
<AddFarmForm onSubmit={handleAddFarm} onCancel={() => setIsDialogOpen(false)} />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@ -107,10 +107,11 @@ export function AppSidebar({ config, ...props }: AppSidebarProps) {
|
||||
async function getUser() {
|
||||
try {
|
||||
const data = await fetchUserMe();
|
||||
console.log(data);
|
||||
setUser({
|
||||
name: data.user.UUID,
|
||||
email: data.user.Email,
|
||||
avatar: data.user.Avatar || "/avatars/avatar.webp",
|
||||
name: data.user.uuid,
|
||||
email: data.user.email,
|
||||
avatar: data.user.avatar || "/avatars/avatar.webp",
|
||||
});
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof Error) {
|
||||
|
||||
@ -31,6 +31,7 @@
|
||||
"@react-oauth/google": "^0.12.1",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@tanstack/react-query": "^5.66.0",
|
||||
"@tanstack/react-table": "^8.21.2",
|
||||
"@vis.gl/react-google-maps": "^1.5.2",
|
||||
"axios": "^1.7.9",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
|
||||
@ -74,6 +74,9 @@ importers:
|
||||
'@tanstack/react-query':
|
||||
specifier: ^5.66.0
|
||||
version: 5.67.3(react@19.0.0)
|
||||
'@tanstack/react-table':
|
||||
specifier: ^8.21.2
|
||||
version: 8.21.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
'@vis.gl/react-google-maps':
|
||||
specifier: ^1.5.2
|
||||
version: 1.5.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
@ -119,6 +122,9 @@ importers:
|
||||
react-hook-form:
|
||||
specifier: ^7.54.2
|
||||
version: 7.54.2(react@19.0.0)
|
||||
react-icons:
|
||||
specifier: ^5.5.0
|
||||
version: 5.5.0(react@19.0.0)
|
||||
recharts:
|
||||
specifier: ^2.15.1
|
||||
version: 2.15.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
@ -974,6 +980,17 @@ packages:
|
||||
peerDependencies:
|
||||
react: ^18 || ^19
|
||||
|
||||
'@tanstack/react-table@8.21.2':
|
||||
resolution: {integrity: sha512-11tNlEDTdIhMJba2RBH+ecJ9l1zgS2kjmexDPAraulc8jeNA4xocSNeyzextT0XJyASil4XsCYlJmf5jEWAtYg==}
|
||||
engines: {node: '>=12'}
|
||||
peerDependencies:
|
||||
react: '>=16.8'
|
||||
react-dom: '>=16.8'
|
||||
|
||||
'@tanstack/table-core@8.21.2':
|
||||
resolution: {integrity: sha512-uvXk/U4cBiFMxt+p9/G7yUWI/UbHYbyghLCjlpWZ3mLeIZiUBSKcUnw9UnKkdRz7Z/N4UBuFLWQdJCjUe7HjvA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
'@types/d3-array@3.2.1':
|
||||
resolution: {integrity: sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==}
|
||||
|
||||
@ -2294,6 +2311,11 @@ packages:
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17 || ^18 || ^19
|
||||
|
||||
react-icons@5.5.0:
|
||||
resolution: {integrity: sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==}
|
||||
peerDependencies:
|
||||
react: '*'
|
||||
|
||||
react-is@16.13.1:
|
||||
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
||||
|
||||
@ -3500,6 +3522,14 @@ snapshots:
|
||||
'@tanstack/query-core': 5.67.3
|
||||
react: 19.0.0
|
||||
|
||||
'@tanstack/react-table@8.21.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
|
||||
dependencies:
|
||||
'@tanstack/table-core': 8.21.2
|
||||
react: 19.0.0
|
||||
react-dom: 19.0.0(react@19.0.0)
|
||||
|
||||
'@tanstack/table-core@8.21.2': {}
|
||||
|
||||
'@types/d3-array@3.2.1': {}
|
||||
|
||||
'@types/d3-color@3.1.3': {}
|
||||
@ -4977,6 +5007,10 @@ snapshots:
|
||||
dependencies:
|
||||
react: 19.0.0
|
||||
|
||||
react-icons@5.5.0(react@19.0.0):
|
||||
dependencies:
|
||||
react: 19.0.0
|
||||
|
||||
react-is@16.13.1: {}
|
||||
|
||||
react-is@18.3.1: {}
|
||||
|
||||
@ -1,131 +1,156 @@
|
||||
export interface Plant {
|
||||
UUID: string;
|
||||
Name: string;
|
||||
Variety: string;
|
||||
AverageHeight: number;
|
||||
DaysToEmerge: number;
|
||||
DaysToFlower: number;
|
||||
DaysToMaturity: number;
|
||||
EstimateLossRate: number;
|
||||
EstimateRevenuePerHU: number;
|
||||
HarvestUnitID: number;
|
||||
HarvestWindow: number;
|
||||
IsPerennial: boolean;
|
||||
LightProfileID: number;
|
||||
OptimalTemp: number;
|
||||
PHValue: number;
|
||||
PlantingDepth: number;
|
||||
PlantingDetail: string;
|
||||
RowSpacing: number;
|
||||
SoilConditionID: number;
|
||||
WaterNeeds: number;
|
||||
CreatedAt: string;
|
||||
UpdatedAt: string;
|
||||
export interface GeoPosition {
|
||||
lat: number;
|
||||
lng: number;
|
||||
}
|
||||
|
||||
export interface Crop {
|
||||
id: string;
|
||||
farmId: string;
|
||||
export interface GeoMarker {
|
||||
type: "marker";
|
||||
position: GeoPosition;
|
||||
}
|
||||
|
||||
export interface GeoPolygon {
|
||||
type: "polygon";
|
||||
path: GeoPosition[];
|
||||
}
|
||||
|
||||
export interface GeoPolyline {
|
||||
type: "polyline";
|
||||
path: GeoPosition[];
|
||||
}
|
||||
|
||||
export type GeoFeatureData = GeoMarker | GeoPolygon | GeoPolyline;
|
||||
|
||||
export interface Plant {
|
||||
uuid: string;
|
||||
name: string;
|
||||
plantedDate: Date;
|
||||
expectedHarvest?: Date;
|
||||
status: string;
|
||||
variety?: string;
|
||||
area?: string;
|
||||
healthScore?: number;
|
||||
progress?: number;
|
||||
averageHeight?: number;
|
||||
daysToEmerge?: number;
|
||||
daysToFlower?: number;
|
||||
daysToMaturity?: number;
|
||||
estimateLossRate?: number;
|
||||
estimateRevenuePerHu?: number;
|
||||
harvestUnitId: number;
|
||||
harvestWindow?: number;
|
||||
isPerennial: boolean;
|
||||
lightProfileId: number;
|
||||
optimalTemp?: number;
|
||||
phValue?: number;
|
||||
plantingDepth?: number;
|
||||
plantingDetail?: string;
|
||||
rowSpacing?: number;
|
||||
soilConditionId: number;
|
||||
waterNeeds?: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface Cropland {
|
||||
UUID: string;
|
||||
Name: string;
|
||||
Status: string;
|
||||
Priority: number;
|
||||
LandSize: number;
|
||||
GrowthStage: string;
|
||||
PlantID: string;
|
||||
FarmID: string;
|
||||
CreatedAt: Date;
|
||||
UpdatedAt: Date;
|
||||
uuid: string;
|
||||
name: string;
|
||||
status: string;
|
||||
priority: number;
|
||||
landSize: number;
|
||||
growthStage: string;
|
||||
plantId: string;
|
||||
farmId: string;
|
||||
geoFeature?: GeoFeatureData | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export interface CropAnalytics {
|
||||
cropId: string;
|
||||
cropName: string;
|
||||
farmId: string;
|
||||
plantName: string;
|
||||
variety?: string;
|
||||
currentStatus: string;
|
||||
growthStage: string;
|
||||
landSize: number;
|
||||
lastUpdated: string;
|
||||
temperature?: number | null;
|
||||
humidity?: number | null;
|
||||
soilMoisture?: number | null;
|
||||
sunlight?: number | null;
|
||||
windSpeed?: number | null;
|
||||
rainfall?: number | null;
|
||||
growthProgress: number;
|
||||
humidity: number;
|
||||
temperature: number;
|
||||
sunlight: number;
|
||||
waterLevel: number;
|
||||
plantHealth: "good" | "warning" | "critical";
|
||||
nextAction: string;
|
||||
nextActionDue: Date;
|
||||
soilMoisture: number;
|
||||
windSpeed: string;
|
||||
rainfall: string;
|
||||
nutrientLevels: {
|
||||
nitrogen: number;
|
||||
phosphorus: number;
|
||||
potassium: number;
|
||||
};
|
||||
plantHealth?: "good" | "warning" | "critical";
|
||||
nextAction?: string | null;
|
||||
nextActionDue?: string | null;
|
||||
nutrientLevels?: {
|
||||
nitrogen: number | null;
|
||||
phosphorus: number | null;
|
||||
potassium: number | null;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export interface Farm {
|
||||
CreatedAt: Date;
|
||||
FarmType: string;
|
||||
Lat: number;
|
||||
Lon: number;
|
||||
Name: string;
|
||||
OwnerID: string;
|
||||
TotalSize: string;
|
||||
UUID: string;
|
||||
UpdatedAt: Date;
|
||||
Crops: Cropland[];
|
||||
uuid: string;
|
||||
name: string;
|
||||
farmType: string;
|
||||
lat: number;
|
||||
lon: number;
|
||||
ownerId: string;
|
||||
totalSize: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
crops: Cropland[];
|
||||
}
|
||||
|
||||
// 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 {
|
||||
ID: number;
|
||||
UUID: string;
|
||||
Username: string;
|
||||
Password: string;
|
||||
Email: string;
|
||||
CreatedAt: string;
|
||||
UpdatedAt: string;
|
||||
Avatar: string;
|
||||
IsActive: boolean;
|
||||
id: number;
|
||||
uuid: string;
|
||||
username?: string;
|
||||
password?: string;
|
||||
email: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
avatar?: string;
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
export type InventoryItem = {
|
||||
id: number;
|
||||
export interface InventoryItem {
|
||||
id: string;
|
||||
userId: string;
|
||||
name: string;
|
||||
category: string;
|
||||
type: string;
|
||||
categoryId: number;
|
||||
category: { id: number; name: string };
|
||||
quantity: number;
|
||||
unit: string;
|
||||
lastUpdated: string;
|
||||
status: string;
|
||||
};
|
||||
export type InventoryItemStatus = {
|
||||
unitId: number;
|
||||
unit: { id: number; name: string };
|
||||
dateAdded: string;
|
||||
statusId: number;
|
||||
status: { id: number; name: string };
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface InventoryStatus {
|
||||
id: number;
|
||||
name: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type CreateInventoryItemInput = Omit<InventoryItem, "id" | "lastUpdated" | "status">;
|
||||
export interface InventoryCategory {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface HarvestUnit {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface CreateInventoryItemInput {
|
||||
name: string;
|
||||
categoryId: number;
|
||||
quantity: number;
|
||||
unitId: number;
|
||||
dateAdded: string;
|
||||
statusId: number;
|
||||
}
|
||||
export type UpdateInventoryItemInput = Partial<CreateInventoryItemInput> & { id: string };
|
||||
|
||||
export interface Blog {
|
||||
id: number;
|
||||
@ -152,8 +177,6 @@ export interface Blog {
|
||||
}[];
|
||||
}
|
||||
|
||||
// ----------- Maps -----------$
|
||||
|
||||
export type OverlayGeometry =
|
||||
| google.maps.Marker
|
||||
| google.maps.Polygon
|
||||
|
||||
Loading…
Reference in New Issue
Block a user