mirror of
https://github.com/ForFarmTeam/ForFarm.git
synced 2025-12-19 14:04: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 { Input } from "@/components/ui/input";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
|
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
|
||||||
import { Crop } from "@/types";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { cropFormSchema } from "@/schemas/form.schema";
|
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 {
|
interface AddCropFormProps {
|
||||||
onSubmit: (data: Partial<Crop>) => Promise<void>;
|
onSubmit: (data: Partial<Cropland>) => Promise<void>; // Expect Partial<Cropland>
|
||||||
onCancel: () => void;
|
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>>({
|
const form = useForm<z.infer<typeof cropFormSchema>>({
|
||||||
resolver: zodResolver(cropFormSchema),
|
resolver: zodResolver(cropFormSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: "",
|
name: "",
|
||||||
plantedDate: "",
|
plantId: "", // Initialize plantId
|
||||||
status: "planned",
|
status: "planned",
|
||||||
|
landSize: 0,
|
||||||
|
growthStage: "Planned",
|
||||||
|
priority: 1,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleSubmit = (values: z.infer<typeof cropFormSchema>) => {
|
const handleSubmit = (values: z.infer<typeof cropFormSchema>) => {
|
||||||
|
// Submit data shaped like Partial<Cropland>
|
||||||
onSubmit({
|
onSubmit({
|
||||||
...values,
|
name: values.name,
|
||||||
plantedDate: new Date(values.plantedDate),
|
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 (
|
return (
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-6">
|
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4">
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="name"
|
name="name"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Crop Name</FormLabel>
|
<FormLabel>Cropland Name</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="Enter crop name" {...field} />
|
<Input placeholder="e.g., North Field Tomatoes" {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Plant Selection */}
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="plantedDate"
|
name="plantId"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Planted Date</FormLabel>
|
<FormLabel>Select Plant</FormLabel>
|
||||||
|
<Select
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
defaultValue={field.value}
|
||||||
|
disabled={isLoadingPlants || isErrorPlants}>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input type="date" {...field} />
|
<SelectTrigger>
|
||||||
|
<SelectValue
|
||||||
|
placeholder={
|
||||||
|
isLoadingPlants
|
||||||
|
? "Loading plants..."
|
||||||
|
: isErrorPlants
|
||||||
|
? "Error loading plants"
|
||||||
|
: "Select the main plant for this cropland"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</SelectTrigger>
|
||||||
</FormControl>
|
</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 />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Status Selection */}
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="status"
|
name="status"
|
||||||
@ -79,6 +148,7 @@ export function AddCropForm({ onSubmit, onCancel }: AddCropFormProps) {
|
|||||||
<SelectItem value="planned">Planned</SelectItem>
|
<SelectItem value="planned">Planned</SelectItem>
|
||||||
<SelectItem value="growing">Growing</SelectItem>
|
<SelectItem value="growing">Growing</SelectItem>
|
||||||
<SelectItem value="harvested">Harvested</SelectItem>
|
<SelectItem value="harvested">Harvested</SelectItem>
|
||||||
|
<SelectItem value="fallow">Fallow</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
@ -86,11 +156,73 @@ export function AddCropForm({ onSubmit, onCancel }: AddCropFormProps) {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex justify-end gap-2">
|
{/* Land Size */}
|
||||||
<Button type="button" variant="outline" onClick={onCancel}>
|
<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
|
Cancel
|
||||||
</Button>
|
</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>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
|
|||||||
@ -1,19 +1,30 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Card, CardContent, CardHeader, CardFooter } from "@/components/ui/card";
|
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 { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Progress } from "@/components/ui/progress";
|
import type { Cropland } from "@/types";
|
||||||
import type { Crop } from "@/types";
|
|
||||||
|
|
||||||
|
// ===================================================================
|
||||||
|
// Component Props: CropCard expects a cropland object and an optional click handler.
|
||||||
|
// ===================================================================
|
||||||
interface CropCardProps {
|
interface CropCardProps {
|
||||||
crop: Crop;
|
crop: Cropland; // Crop data conforming to the Cropland type
|
||||||
onClick?: () => void;
|
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) {
|
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: {
|
growing: {
|
||||||
bg: "bg-green-50 dark:bg-green-900",
|
bg: "bg-green-50 dark:bg-green-900",
|
||||||
text: "text-green-600 dark:text-green-300",
|
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",
|
text: "text-blue-600 dark:text-blue-300",
|
||||||
border: "border-blue-200",
|
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 (
|
return (
|
||||||
<Card
|
<Card
|
||||||
onClick={onClick}
|
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">
|
<CardHeader className="p-4 pb-0">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<Badge variant="outline" className={`capitalize ${statusColor.bg} ${statusColor.text} ${statusColor.border}`}>
|
<Badge variant="outline" className={`capitalize ${statusColor.bg} ${statusColor.text} ${statusColor.border}`}>
|
||||||
{crop.status}
|
{crop.status || "Unknown"}
|
||||||
</Badge>
|
</Badge>
|
||||||
<div className="flex items-center text-xs text-muted-foreground">
|
<div className="flex items-center text-xs text-muted-foreground">
|
||||||
<Calendar className="h-3 w-3 mr-1" />
|
<Calendar className="h-3 w-3 mr-1" />
|
||||||
{crop.plantedDate.toLocaleDateString()}
|
{displayDate}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="p-4">
|
<CardContent className="p-4 flex-grow">
|
||||||
<div className="flex items-start gap-3">
|
<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`}>
|
{/* ... icon ... */}
|
||||||
<Sprout className={`h-5 w-5 ${statusColor.text}`} />
|
|
||||||
</div>
|
|
||||||
<div className="flex-1">
|
<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">
|
<p className="text-sm text-muted-foreground mb-2">
|
||||||
{crop.variety} • {crop.area}
|
{crop.growthStage || "N/A"} • {displayArea} {/* Use camelCase growthStage */}
|
||||||
</p>
|
</p>
|
||||||
|
{crop.growthStage && (
|
||||||
{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" && (
|
|
||||||
<div className="flex items-center mt-3 text-sm">
|
<div className="flex items-center mt-3 text-sm">
|
||||||
<div className="flex items-center gap-1 text-green-600 dark:text-green-300">
|
<div className="flex items-center gap-1 text-muted-foreground">
|
||||||
<BarChart className="h-3.5 w-3.5" />
|
<Layers className="h-3.5 w-3.5" />
|
||||||
<span className="font-medium">Health: {crop.healthScore}%</span>
|
{/* Use camelCase growthStage */}
|
||||||
|
<span className="font-medium">Stage: {crop.growthStage}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
<CardFooter className="p-4 pt-0">
|
<CardFooter className="p-4 pt-0 mt-auto">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
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" />
|
View details <ArrowRight className="h-3.5 w-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
|
|||||||
@ -1,125 +1,301 @@
|
|||||||
|
// crop-dialog.tsx
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import React, { useState, useMemo, useCallback, useEffect } from "react";
|
||||||
import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
|
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 { Button } from "@/components/ui/button";
|
||||||
import { Card } from "@/components/ui/card";
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
import { Check, MapPin } from "lucide-react";
|
import {
|
||||||
|
Check,
|
||||||
|
Sprout,
|
||||||
|
AlertTriangle,
|
||||||
|
Loader2,
|
||||||
|
CalendarDays,
|
||||||
|
Thermometer,
|
||||||
|
Droplets,
|
||||||
|
MapPin,
|
||||||
|
Maximize,
|
||||||
|
} from "lucide-react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import type { Crop } from "@/types";
|
// Import the updated/new types
|
||||||
import { VisuallyHidden } from "@radix-ui/react-visually-hidden";
|
import type { Cropland, GeoFeatureData, GeoPosition } from "@/types";
|
||||||
import GoogleMapWithDrawing from "@/components/google-map-with-drawing";
|
import { PlantResponse } from "@/api/plant";
|
||||||
|
import { getPlants } from "@/api/plant";
|
||||||
interface Plant {
|
// Import the map component and the ShapeData type (ensure ShapeData in types.ts matches this)
|
||||||
id: string;
|
import GoogleMapWithDrawing, { type ShapeData } from "@/components/google-map-with-drawing";
|
||||||
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",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
interface CropDialogProps {
|
interface CropDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (open: boolean) => void;
|
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) {
|
export function CropDialog({ open, onOpenChange, onSubmit, isSubmitting }: CropDialogProps) {
|
||||||
const [selectedPlant, setSelectedPlant] = useState<string | null>(null);
|
// --- State ---
|
||||||
const [location, setLocation] = useState({ lat: 13.7563, lng: 100.5018 }); // Bangkok coordinates
|
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
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
// --- Load Google Maps Geometry Library ---
|
||||||
if (!selectedPlant) return;
|
const geometryLib = useMapsLibrary("geometry");
|
||||||
|
|
||||||
await onSubmit({
|
// --- Fetch Plants ---
|
||||||
name: plants.find((p) => p.id === selectedPlant)?.name || "",
|
const {
|
||||||
plantedDate: new Date(),
|
data: plantData,
|
||||||
status: "planned",
|
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]);
|
||||||
|
|
||||||
setSelectedPlant(null);
|
// --- Reset State on Dialog Close ---
|
||||||
onOpenChange(false);
|
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 () => {
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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 (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<VisuallyHidden>
|
<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">
|
||||||
<DialogTitle></DialogTitle>
|
<DialogHeader className="p-6 pb-0">
|
||||||
</VisuallyHidden>
|
<DialogTitle className="text-xl font-semibold">Create New Cropland</DialogTitle>
|
||||||
<DialogContent className="sm:max-w-[900px] p-0">
|
<DialogDescription>
|
||||||
<div className="grid md:grid-cols-2 h-[600px]">
|
Select a plant and draw the cropland boundary or mark its location on the map.
|
||||||
{/* Left side - Plant Selection */}
|
</DialogDescription>
|
||||||
<div className="p-6 overflow-y-auto border-r dark:border-slate-700">
|
</DialogHeader>
|
||||||
<h2 className="text-lg font-semibold mb-4">Select Plant to Grow</h2>
|
|
||||||
<div className="space-y-4">
|
<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>
|
||||||
|
)}
|
||||||
|
{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) => (
|
{plants.map((plant) => (
|
||||||
<Card
|
<Card
|
||||||
key={plant.id}
|
key={plant.uuid}
|
||||||
className={cn(
|
className={cn(
|
||||||
"p-4 cursor-pointer hover:bg-muted/50 dark:hover:bg-muted/40 transition-colors",
|
"p-3 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"
|
selectedPlantUUID === plant.uuid &&
|
||||||
|
"border-2 border-primary dark:border-primary dark:bg-primary/5 bg-primary/5"
|
||||||
)}
|
)}
|
||||||
onClick={() => setSelectedPlant(plant.id)}>
|
onClick={() => setSelectedPlantUUID(plant.uuid)}>
|
||||||
<div className="flex items-center gap-4">
|
<CardContent className="p-0">
|
||||||
<img
|
<div className="flex items-start gap-3">
|
||||||
src={plant.image || "/placeholder.svg"}
|
<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">
|
||||||
alt={plant.name}
|
<Sprout className="w-8 h-8 text-green-600 dark:text-green-400" />
|
||||||
className="w-20 h-20 rounded-lg object-cover"
|
</div>
|
||||||
/>
|
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h3 className="font-medium">{plant.name}</h3>
|
<h4 className="font-medium text-sm">
|
||||||
{selectedPlant === plant.id && <Check className="h-4 w-4 text-primary" />}
|
{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>
|
||||||
<p className="text-sm text-muted-foreground">Growth time: {plant.growthTime}</p>
|
<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>
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right side - Map */}
|
{/* Right Side: Map */}
|
||||||
<div className="relative">
|
<div className="md:col-span-8 lg:col-span-9 p-6 pt-2 flex flex-col overflow-hidden">
|
||||||
<div className="absolute inset-0 bg-muted/10 dark:bg-muted/20">
|
<h3 className="text-md font-medium mb-4">2. Define Boundary / Location</h3>
|
||||||
<div className="h-full w-full flex items-center justify-center">
|
<div className="flex-grow bg-muted/30 dark:bg-muted/20 rounded-md border dark:border-slate-700 overflow-hidden relative">
|
||||||
<GoogleMapWithDrawing />
|
<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>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Dialog Footer */}
|
||||||
<div className="absolute bottom-0 left-0 right-0 p-4 bg-background dark:bg-background border-t dark:border-slate-700">
|
<DialogFooter className="p-6 pt-4 border-t dark:border-slate-700 mt-auto">
|
||||||
<div className="flex justify-end gap-2">
|
<Button variant="outline" onClick={() => onOpenChange(false)} disabled={isSubmitting}>
|
||||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleSubmit} disabled={!selectedPlant}>
|
{/* Disable submit if no plant OR no feature is selected */}
|
||||||
Plant Crop
|
<Button onClick={handleSubmit} disabled={!selectedPlantUUID || !geoFeature || isSubmitting}>
|
||||||
|
{isSubmitting ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
Creating...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
"Create Cropland"
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</DialogFooter>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -4,12 +4,12 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/u
|
|||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { LineChart, Sprout, Droplets, Sun } from "lucide-react";
|
import { LineChart, Sprout, Droplets, Sun } from "lucide-react";
|
||||||
import type { Crop, CropAnalytics } from "@/types";
|
import type { CropAnalytics, Cropland } from "@/types";
|
||||||
|
|
||||||
interface AnalyticsDialogProps {
|
interface AnalyticsDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
crop: Crop;
|
crop: Cropland;
|
||||||
analytics: CropAnalytics;
|
analytics: CropAnalytics;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,30 +1,29 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useMemo, useState } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter, useParams } from "next/navigation";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import {
|
import {
|
||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
Sprout,
|
|
||||||
LineChart,
|
LineChart,
|
||||||
MessageSquare,
|
|
||||||
Settings,
|
Settings,
|
||||||
Droplets,
|
Droplets,
|
||||||
Sun,
|
Sun,
|
||||||
ThermometerSun,
|
ThermometerSun,
|
||||||
Timer,
|
Timer,
|
||||||
ListCollapse,
|
ListCollapse,
|
||||||
Calendar,
|
|
||||||
Leaf,
|
Leaf,
|
||||||
CloudRain,
|
CloudRain,
|
||||||
Wind,
|
Wind,
|
||||||
|
Home,
|
||||||
|
ChevronRight,
|
||||||
|
AlertTriangle,
|
||||||
|
Loader2,
|
||||||
|
LeafIcon,
|
||||||
|
History,
|
||||||
|
Bot,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import {
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
CardDescription,
|
|
||||||
} from "@/components/ui/card";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { Progress } from "@/components/ui/progress";
|
import { Progress } from "@/components/ui/progress";
|
||||||
@ -32,56 +31,121 @@ import { Badge } from "@/components/ui/badge";
|
|||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
import { ChatbotDialog } from "./chatbot-dialog";
|
import { ChatbotDialog } from "./chatbot-dialog";
|
||||||
import { AnalyticsDialog } from "./analytics-dialog";
|
import { AnalyticsDialog } from "./analytics-dialog";
|
||||||
import {
|
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||||
HoverCard,
|
import type { Cropland, CropAnalytics, Farm } from "@/types";
|
||||||
HoverCardContent,
|
import { getFarm } from "@/api/farm";
|
||||||
HoverCardTrigger,
|
import { getPlants, PlantResponse } from "@/api/plant";
|
||||||
} from "@/components/ui/hover-card";
|
import { getCropById, fetchCropAnalytics } from "@/api/crop";
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
|
||||||
import type { Crop, CropAnalytics } from "@/types";
|
|
||||||
import GoogleMapWithDrawing from "@/components/google-map-with-drawing";
|
import GoogleMapWithDrawing from "@/components/google-map-with-drawing";
|
||||||
import { fetchCropById, fetchAnalyticsByCropId } from "@/api/farm";
|
|
||||||
|
|
||||||
interface CropDetailPageParams {
|
export default function CropDetailPage() {
|
||||||
farmId: string;
|
|
||||||
cropId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function CropDetailPage({
|
|
||||||
params,
|
|
||||||
}: {
|
|
||||||
params: Promise<CropDetailPageParams>;
|
|
||||||
}) {
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [crop, setCrop] = useState<Crop | null>(null);
|
const params = useParams<{ farmId: string; cropId: string }>();
|
||||||
const [analytics, setAnalytics] = useState<CropAnalytics | null>(null);
|
const { farmId, cropId } = params;
|
||||||
|
|
||||||
const [isChatOpen, setIsChatOpen] = useState(false);
|
const [isChatOpen, setIsChatOpen] = useState(false);
|
||||||
const [isAnalyticsOpen, setIsAnalyticsOpen] = useState(false);
|
const [isAnalyticsOpen, setIsAnalyticsOpen] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
// --- Fetch Farm Data ---
|
||||||
async function fetchData() {
|
const { data: farm, isLoading: isLoadingFarm } = useQuery<Farm>({
|
||||||
const resolvedParams = await params;
|
queryKey: ["farm", farmId],
|
||||||
const cropData = await fetchCropById(resolvedParams.cropId);
|
queryFn: () => getFarm(farmId),
|
||||||
const analyticsData = await fetchAnalyticsByCropId(resolvedParams.cropId);
|
enabled: !!farmId,
|
||||||
setCrop(cropData);
|
staleTime: 5 * 60 * 1000,
|
||||||
setAnalytics(analyticsData);
|
});
|
||||||
}
|
|
||||||
fetchData();
|
|
||||||
}, [params]);
|
|
||||||
|
|
||||||
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 (
|
return (
|
||||||
<div className="min-h-screen flex items-center justify-center bg-background text-foreground">
|
<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>
|
</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 = {
|
const healthColors = {
|
||||||
good: "text-green-500 bg-green-50 dark:bg-green-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",
|
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",
|
critical: "text-red-500 bg-red-50 dark:bg-red-900 border-red-200",
|
||||||
};
|
};
|
||||||
|
const healthStatus = analytics?.plantHealth || "good";
|
||||||
|
|
||||||
const quickActions = [
|
const quickActions = [
|
||||||
{
|
{
|
||||||
@ -93,7 +157,7 @@ export default function CropDetailPage({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Chat Assistant",
|
title: "Chat Assistant",
|
||||||
icon: MessageSquare,
|
icon: Bot,
|
||||||
description: "Get help and advice",
|
description: "Get help and advice",
|
||||||
onClick: () => setIsChatOpen(true),
|
onClick: () => setIsChatOpen(true),
|
||||||
color: "bg-green-50 dark:bg-green-900 text-green-600 dark:text-green-300",
|
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",
|
title: "Crop Details",
|
||||||
icon: ListCollapse,
|
icon: ListCollapse,
|
||||||
description: "View detailed information",
|
description: "View detailed information",
|
||||||
onClick: () => console.log("Details clicked"),
|
onClick: () => console.log("Details clicked - Placeholder"),
|
||||||
color:
|
color: "bg-purple-50 dark:bg-purple-900 text-purple-600 dark:text-purple-300",
|
||||||
"bg-purple-50 dark:bg-purple-900 text-purple-600 dark:text-purple-300",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Settings",
|
title: "Settings",
|
||||||
icon: Settings,
|
icon: Settings,
|
||||||
description: "Configure crop 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",
|
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 (
|
return (
|
||||||
<div className="min-h-screen bg-background text-foreground">
|
<div className="min-h-screen bg-background text-foreground">
|
||||||
<div className="container max-w-7xl p-6 mx-auto">
|
<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 */}
|
{/* Header */}
|
||||||
<div className="flex flex-col gap-6 mb-8">
|
<div className="flex flex-col gap-6 mb-8">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="outline"
|
||||||
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"
|
size="sm"
|
||||||
onClick={() => router.back()}
|
className="w-fit gap-2 text-muted-foreground"
|
||||||
>
|
onClick={() => router.push(`/farms/${farmId}`)}>
|
||||||
<ArrowLeft className="h-4 w-4" /> Back to Farm
|
<ArrowLeft className="h-4 w-4" /> Back to Farm
|
||||||
</Button>
|
</Button>
|
||||||
<HoverCard>
|
{/* Hover Card (removed for simplicity, add back if needed) */}
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col md:flex-row justify-between gap-4">
|
<div className="flex flex-col md:flex-row justify-between gap-4">
|
||||||
<div className="space-y-1">
|
<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">
|
<p className="text-muted-foreground">
|
||||||
{crop.variety} • {crop.area}
|
{plant?.variety || "Unknown Variety"} • {displayArea} {/* Use camelCase */}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<div className="flex flex-col items-end">
|
<div className="flex flex-col items-end">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Badge
|
<Badge variant="outline" className={`${healthColors[healthStatus]} border capitalize`}>
|
||||||
variant="outline"
|
{cropland.status} {/* Use camelCase */}
|
||||||
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>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
{crop.expectedHarvest ? (
|
{expectedHarvestDate ? (
|
||||||
<p className="text-sm text-muted-foreground mt-1">
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
Expected harvest:{" "}
|
Expected harvest: {expectedHarvestDate.toLocaleDateString()}
|
||||||
{crop.expectedHarvest.toLocaleDateString()}
|
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-sm text-muted-foreground mt-1">
|
<p className="text-sm text-muted-foreground mt-1">Expected harvest date not available</p>
|
||||||
Expected harvest date not available
|
|
||||||
</p>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -208,138 +265,123 @@ export default function CropDetailPage({
|
|||||||
<Button
|
<Button
|
||||||
key={action.title}
|
key={action.title}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className={`h-auto p-4 flex flex-col items-center gap-3 transition-all group ${action.color} hover:scale-105`}
|
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}
|
onClick={action.onClick}>
|
||||||
>
|
|
||||||
<div
|
<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" />
|
<action.icon className="h-5 w-5" />
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="font-medium mb-1">{action.title}</div>
|
<div className="font-medium mb-1">{action.title}</div>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">{action.description}</p>
|
||||||
{action.description}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Environmental Metrics */}
|
{/* Environmental Metrics */}
|
||||||
<Card className="border-green-100 dark:border-green-700">
|
<Card className="border-border/30">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Environmental Conditions</CardTitle>
|
<CardTitle>Environmental Conditions</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>Real-time monitoring data</CardDescription>
|
||||||
Real-time monitoring of growing conditions
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="grid gap-6">
|
<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,
|
icon: ThermometerSun,
|
||||||
label: "Temperature",
|
label: "Temperature",
|
||||||
value: `${analytics.temperature}°C`,
|
value: analytics?.temperature ? `${analytics.temperature}°C` : "N/A",
|
||||||
color: "text-orange-500 dark:text-orange-300",
|
color: "text-orange-500 dark:text-orange-300",
|
||||||
bg: "bg-orange-50 dark:bg-orange-900",
|
bg: "bg-orange-50 dark:bg-orange-900",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: Droplets,
|
icon: Droplets,
|
||||||
label: "Humidity",
|
label: "Humidity",
|
||||||
value: `${analytics.humidity}%`,
|
value: analytics?.humidity ? `${analytics.humidity}%` : "N/A",
|
||||||
color: "text-blue-500 dark:text-blue-300",
|
color: "text-blue-500 dark:text-blue-300",
|
||||||
bg: "bg-blue-50 dark:bg-blue-900",
|
bg: "bg-blue-50 dark:bg-blue-900",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: Sun,
|
icon: Sun,
|
||||||
label: "Sunlight",
|
label: "Sunlight",
|
||||||
value: `${analytics.sunlight}%`,
|
value: analytics?.sunlight ? `${analytics.sunlight}%` : "N/A",
|
||||||
color: "text-yellow-500 dark:text-yellow-300",
|
color: "text-yellow-500 dark:text-yellow-300",
|
||||||
bg: "bg-yellow-50 dark:bg-yellow-900",
|
bg: "bg-yellow-50 dark:bg-yellow-900",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: Leaf,
|
icon: Leaf,
|
||||||
label: "Soil Moisture",
|
label: "Soil Moisture",
|
||||||
value: `${analytics.soilMoisture}%`,
|
value: analytics?.soilMoisture ? `${analytics.soilMoisture}%` : "N/A",
|
||||||
color: "text-green-500 dark:text-green-300",
|
color: "text-green-500 dark:text-green-300",
|
||||||
bg: "bg-green-50 dark:bg-green-900",
|
bg: "bg-green-50 dark:bg-green-900",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: Wind,
|
icon: Wind,
|
||||||
label: "Wind Speed",
|
label: "Wind Speed",
|
||||||
value: analytics.windSpeed,
|
value: analytics?.windSpeed ?? "N/A",
|
||||||
color: "text-gray-500 dark:text-gray-300",
|
color: "text-gray-500 dark:text-gray-300",
|
||||||
bg: "bg-gray-50 dark:bg-gray-900",
|
bg: "bg-gray-50 dark:bg-gray-900",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: CloudRain,
|
icon: CloudRain,
|
||||||
label: "Rainfall",
|
label: "Rainfall",
|
||||||
value: analytics.rainfall,
|
value: analytics?.rainfall ?? "N/A",
|
||||||
color: "text-indigo-500 dark:text-indigo-300",
|
color: "text-indigo-500 dark:text-indigo-300",
|
||||||
bg: "bg-indigo-50 dark:bg-indigo-900",
|
bg: "bg-indigo-50 dark:bg-indigo-900",
|
||||||
},
|
},
|
||||||
].map((metric) => (
|
].map((metric) => (
|
||||||
<Card
|
<Card key={metric.label} className="border-border/30 shadow-none bg-card dark:bg-slate-800">
|
||||||
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"
|
|
||||||
>
|
|
||||||
<CardContent className="p-4">
|
<CardContent className="p-4">
|
||||||
<div className="flex items-start gap-4">
|
<div className="flex items-start gap-3">
|
||||||
<div className={`p-2 rounded-lg ${metric.bg}`}>
|
<div className={`p-2 rounded-lg ${metric.bg}/50`}>
|
||||||
<metric.icon
|
<metric.icon className={`h-4 w-4 ${metric.color}`} />
|
||||||
className={`h-4 w-4 ${metric.color}`}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium text-muted-foreground">
|
<p className="text-sm font-medium text-muted-foreground">{metric.label}</p>
|
||||||
{metric.label}
|
<p className="text-xl font-semibold tracking-tight">{metric.value}</p>
|
||||||
</p>
|
|
||||||
<p className="text-2xl font-semibold tracking-tight">
|
|
||||||
{metric.value}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Separator />
|
<Separator />
|
||||||
|
|
||||||
{/* Growth Progress */}
|
{/* Growth Progress */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex justify-between text-sm">
|
<div className="flex justify-between text-sm">
|
||||||
<span className="font-medium">Growth Progress</span>
|
<span className="font-medium">Growth Progress</span>
|
||||||
<span className="text-muted-foreground">
|
<span className="text-muted-foreground">{growthProgress}%</span>
|
||||||
{analytics.growthProgress}%
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<Progress
|
<Progress value={growthProgress} className="h-2" />
|
||||||
value={analytics.growthProgress}
|
<p className="text-xs text-muted-foreground">
|
||||||
className="h-2"
|
Based on {daysToMaturity ? `${daysToMaturity} days` : "N/A"} to maturity.
|
||||||
/>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Next Action Card */}
|
{/* 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">
|
<CardContent className="p-4">
|
||||||
<div className="flex items-start gap-4">
|
<div className="flex items-start gap-4">
|
||||||
<div className="p-2 rounded-lg bg-green-100 dark:bg-green-800">
|
<div className="p-2 rounded-lg bg-blue-100 dark:bg-blue-800">
|
||||||
<Timer className="h-4 w-4 text-green-600 dark:text-green-300" />
|
<Timer className="h-4 w-4 text-blue-600 dark:text-blue-300" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium mb-1">
|
<p className="font-medium mb-1">Next Action Required</p>
|
||||||
Next Action Required
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
{analytics.nextAction}
|
{analytics?.nextAction || "Check crop status"}
|
||||||
</p>
|
</p>
|
||||||
|
{analytics?.nextActionDue && (
|
||||||
<p className="text-xs text-muted-foreground mt-1">
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
Due by{" "}
|
Due by {new Date(analytics.nextActionDue).toLocaleDateString()}
|
||||||
{analytics.nextActionDue.toLocaleDateString()}
|
|
||||||
</p>
|
</p>
|
||||||
|
)}
|
||||||
|
{!analytics?.nextAction && (
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">No immediate actions required.</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@ -349,13 +391,18 @@ export default function CropDetailPage({
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Map Section */}
|
{/* Map Section */}
|
||||||
<Card className="border-green-100 dark:border-green-700">
|
<Card className="border-border/30">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Field Map</CardTitle>
|
<CardTitle>Cropland Location / Boundary</CardTitle>
|
||||||
<CardDescription>View and manage crop location</CardDescription>
|
<CardDescription>Visual representation on the farm</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="p-0 h-[400px]">
|
<CardContent className="p-0 h-[400px] overflow-hidden rounded-b-lg">
|
||||||
<GoogleMapWithDrawing />
|
{/* TODO: Ensure GoogleMapWithDrawing correctly parses and displays GeoFeatureData */}
|
||||||
|
<GoogleMapWithDrawing
|
||||||
|
initialFeatures={cropland.geoFeature ? [cropland.geoFeature] : undefined}
|
||||||
|
drawingMode={null}
|
||||||
|
editable={false}
|
||||||
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
@ -363,83 +410,64 @@ export default function CropDetailPage({
|
|||||||
{/* Right Column */}
|
{/* Right Column */}
|
||||||
<div className="md:col-span-4 space-y-6">
|
<div className="md:col-span-4 space-y-6">
|
||||||
{/* Nutrient Levels */}
|
{/* Nutrient Levels */}
|
||||||
<Card className="border-green-100 dark:border-green-700">
|
<Card className="border-border/30">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Nutrient Levels</CardTitle>
|
<CardTitle className="flex items-center gap-2">
|
||||||
<CardDescription>Current soil composition</CardDescription>
|
<LeafIcon className="h-5 w-5 text-amber-600 dark:text-amber-400" />
|
||||||
|
Nutrient Levels
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>Soil composition (if available)</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{[
|
{[
|
||||||
{
|
{
|
||||||
name: "Nitrogen (N)",
|
name: "Nitrogen (N)",
|
||||||
value: analytics.nutrientLevels.nitrogen,
|
value: analytics?.nutrientLevels?.nitrogen,
|
||||||
color: "bg-blue-500 dark:bg-blue-700",
|
color: "bg-blue-500 dark:bg-blue-700",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Phosphorus (P)",
|
name: "Phosphorus (P)",
|
||||||
value: analytics.nutrientLevels.phosphorus,
|
value: analytics?.nutrientLevels?.phosphorus,
|
||||||
color: "bg-yellow-500 dark:bg-yellow-700",
|
color: "bg-yellow-500 dark:bg-yellow-700",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Potassium (K)",
|
name: "Potassium (K)",
|
||||||
value: analytics.nutrientLevels.potassium,
|
value: analytics?.nutrientLevels?.potassium,
|
||||||
color: "bg-green-500 dark:bg-green-700",
|
color: "bg-green-500 dark:bg-green-700",
|
||||||
},
|
},
|
||||||
].map((nutrient) => (
|
].map((nutrient) => (
|
||||||
<div key={nutrient.name} className="space-y-2">
|
<div key={nutrient.name} className="space-y-2">
|
||||||
<div className="flex justify-between text-sm">
|
<div className="flex justify-between text-sm">
|
||||||
<span className="font-medium">{nutrient.name}</span>
|
<span className="font-medium">{nutrient.name}</span>
|
||||||
<span className="text-muted-foreground">
|
<span className="text-muted-foreground">{nutrient.value ?? "N/A"}%</span>
|
||||||
{nutrient.value}%
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<Progress
|
<Progress
|
||||||
value={nutrient.value}
|
value={nutrient.value ?? 0}
|
||||||
className={`h-2 ${nutrient.color}`}
|
className={`h-2 ${
|
||||||
|
nutrient.value !== null && nutrient.value !== undefined ? nutrient.color : "bg-muted"
|
||||||
|
}`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
{!analytics?.nutrientLevels && (
|
||||||
|
<p className="text-center text-sm text-muted-foreground py-4">Nutrient data not available.</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Recent Activity */}
|
{/* Recent Activity */}
|
||||||
<Card className="border-green-100 dark:border-green-700">
|
<Card className="border-border/30">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Recent Activity</CardTitle>
|
<CardTitle className="flex items-center gap-2">
|
||||||
<CardDescription>Latest updates and changes</CardDescription>
|
<History className="h-5 w-5 text-purple-600 dark:text-purple-400" />
|
||||||
|
Recent Activity
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>Latest updates (placeholder)</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<ScrollArea className="h-[300px] pr-4">
|
<ScrollArea className="h-[300px] pr-4">
|
||||||
{[...Array(5)].map((_, i) => (
|
<div className="text-center py-10 text-muted-foreground">No recent activity logged.</div>
|
||||||
<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>
|
|
||||||
))}
|
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@ -447,38 +475,30 @@ export default function CropDetailPage({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Dialogs */}
|
{/* Dialogs */}
|
||||||
<ChatbotDialog
|
<ChatbotDialog open={isChatOpen} onOpenChange={setIsChatOpen} cropName={cropland.name || "this crop"} />
|
||||||
open={isChatOpen}
|
{/* Ensure AnalyticsDialog uses the correct props */}
|
||||||
onOpenChange={setIsChatOpen}
|
{analytics && (
|
||||||
cropName={crop.name}
|
|
||||||
/>
|
|
||||||
<AnalyticsDialog
|
<AnalyticsDialog
|
||||||
open={isAnalyticsOpen}
|
open={isAnalyticsOpen}
|
||||||
onOpenChange={setIsAnalyticsOpen}
|
onOpenChange={setIsAnalyticsOpen}
|
||||||
crop={crop}
|
// The dialog expects a `Crop` type, but we have `Cropland` and `CropAnalytics`
|
||||||
analytics={analytics}
|
// 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>
|
||||||
</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 React, { useMemo, useState } from "react";
|
||||||
// import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
// import {
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
// ArrowLeft,
|
import {
|
||||||
// MapPin,
|
ArrowLeft,
|
||||||
// Plus,
|
MapPin,
|
||||||
// Sprout,
|
Plus,
|
||||||
// Calendar,
|
Sprout,
|
||||||
// LayoutGrid,
|
Calendar,
|
||||||
// AlertTriangle,
|
LayoutGrid,
|
||||||
// Loader2,
|
AlertTriangle,
|
||||||
// Home,
|
Loader2,
|
||||||
// ChevronRight,
|
Home,
|
||||||
// Droplets,
|
ChevronRight,
|
||||||
// Sun,
|
Sun,
|
||||||
// Wind,
|
} from "lucide-react";
|
||||||
// } from "lucide-react";
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
// import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
import { useParams } from "next/navigation";
|
||||||
// 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 { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
||||||
// * Used in Next.js; params is now a Promise and must be unwrapped with React.use()
|
import { Button } from "@/components/ui/button";
|
||||||
// */
|
import { CropDialog } from "./crop-dialog";
|
||||||
// interface FarmDetailPageProps {
|
import { CropCard } from "./crop-card";
|
||||||
// params: Promise<{ farmId: string }>;
|
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)
|
// Page Component: FarmDetailPage
|
||||||
// const resolvedParams = React.use(params);
|
// - 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);
|
// Local State for dialog and crop filter management
|
||||||
// const [crops, setCrops] = useState<Crop[]>([]);
|
// ---------------------------------------------------------------
|
||||||
// const [isDialogOpen, setIsDialogOpen] = useState(false);
|
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||||
// const [isLoading, setIsLoading] = useState(true);
|
const [activeFilter, setActiveFilter] = useState<string>("all");
|
||||||
// const [error, setError] = useState<string | null>(null);
|
|
||||||
// const [activeFilter, setActiveFilter] = useState<string>("all");
|
|
||||||
|
|
||||||
// // Fetch farm details on initial render using the resolved params
|
// ---------------------------------------------------------------
|
||||||
// useEffect(() => {
|
// Data fetching: Farm details and Crops using React Query.
|
||||||
// async function loadFarmDetails() {
|
// - See: https://tanstack.com/query
|
||||||
// try {
|
// ---------------------------------------------------------------
|
||||||
// setIsLoading(true);
|
const {
|
||||||
// setError(null);
|
data: farm,
|
||||||
// const { farm, crops } = await fetchFarmDetails(resolvedParams.farmId);
|
isLoading: isLoadingFarm,
|
||||||
// setFarm(farm);
|
isError: isErrorFarm,
|
||||||
// setCrops(crops);
|
error: errorFarm,
|
||||||
// } catch (err) {
|
} = useQuery<Farm>({
|
||||||
// if (err instanceof Error) {
|
queryKey: ["farm", farmId],
|
||||||
// if (err.message === "FARM_NOT_FOUND") {
|
queryFn: () => getFarm(farmId),
|
||||||
// router.push("/not-found");
|
enabled: !!farmId,
|
||||||
// return;
|
staleTime: 60 * 1000,
|
||||||
// }
|
});
|
||||||
// setError(err.message);
|
|
||||||
// } else {
|
|
||||||
// setError("An unknown error occurred");
|
|
||||||
// }
|
|
||||||
// } finally {
|
|
||||||
// setIsLoading(false);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// loadFarmDetails();
|
const {
|
||||||
// }, [resolvedParams.farmId, router]);
|
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.
|
// Mutation: Create Crop
|
||||||
// */
|
// - After creation, invalidate queries to refresh data.
|
||||||
// const handleAddCrop = async (data: Partial<Crop>) => {
|
// ---------------------------------------------------------------
|
||||||
// try {
|
const croplands = useMemo(() => cropData?.croplands || [], [cropData]);
|
||||||
// // Simulate API delay
|
|
||||||
// await new Promise((resolve) => setTimeout(resolve, 800));
|
|
||||||
|
|
||||||
// const newCrop: Crop = {
|
const mutation = useMutation({
|
||||||
// id: Math.random().toString(36).substr(2, 9),
|
mutationFn: (newCropData: Partial<Cropland>) => createCrop({ ...newCropData, farmId: farmId }), // Pass farmId here
|
||||||
// farmId: farm!.id,
|
onSuccess: (newlyCreatedCrop) => {
|
||||||
// name: data.name!,
|
console.log("Successfully created crop:", newlyCreatedCrop);
|
||||||
// plantedDate: data.plantedDate!,
|
queryClient.invalidateQueries({ queryKey: ["crops", farmId] });
|
||||||
// status: data.status!,
|
queryClient.invalidateQueries({ queryKey: ["farm", farmId] }); // Invalidate farm too to update crop count potentially
|
||||||
// variety: data.variety || "Standard",
|
setIsDialogOpen(false);
|
||||||
// area: data.area || "0 hectares",
|
},
|
||||||
// healthScore: data.status === "growing" ? 85 : 0,
|
onError: (error) => {
|
||||||
// progress: data.status === "growing" ? 10 : 0,
|
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) {
|
// Determine combined loading and error states from individual queries.
|
||||||
// setFarm({ ...farm, crops: farm.crops + 1 });
|
// ---------------------------------------------------------------
|
||||||
// }
|
const isLoading = isLoadingFarm || isLoadingCrops;
|
||||||
|
const isError = isErrorFarm || isErrorCrops;
|
||||||
|
const error = errorFarm || errorCrops;
|
||||||
|
|
||||||
// setIsDialogOpen(false);
|
// ---------------------------------------------------------------
|
||||||
// } catch (err) {
|
// Filter crops based on the active filter tab.
|
||||||
// setError("Failed to add crop. Please try again.");
|
// ---------------------------------------------------------------
|
||||||
// }
|
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 = {
|
// Derive the unique statuses from the crops list for the tabs.
|
||||||
// all: crops.length,
|
// ---------------------------------------------------------------
|
||||||
// growing: crops.filter((crop) => crop.status === "growing").length,
|
const availableStatuses = useMemo(() => {
|
||||||
// planned: crops.filter((crop) => crop.status === "planned").length,
|
return ["all", ...new Set(croplands.map((crop) => crop.status.toLowerCase()))]; // Use camelCase status
|
||||||
// harvested: crops.filter((crop) => crop.status === "harvested").length,
|
}, [croplands]);
|
||||||
// };
|
|
||||||
|
|
||||||
// return (
|
// ===================================================================
|
||||||
// <div className="min-h-screen bg-background text-foreground">
|
// Render: Main page layout segmented into breadcrumbs, farm cards,
|
||||||
// <div className="container max-w-7xl p-6 mx-auto">
|
// crop management, and the crop add dialog.
|
||||||
// <div className="flex flex-col gap-6">
|
// ===================================================================
|
||||||
// {/* Breadcrumbs */}
|
return (
|
||||||
// <nav className="flex items-center text-sm text-muted-foreground">
|
<div className="min-h-screen bg-background text-foreground">
|
||||||
// <Button
|
<div className="container max-w-7xl p-6 mx-auto">
|
||||||
// variant="link"
|
<div className="flex flex-col gap-6">
|
||||||
// className="p-0 h-auto font-normal text-muted-foreground"
|
{/* ------------------------------
|
||||||
// onClick={() => router.push("/")}>
|
Breadcrumbs Navigation Section
|
||||||
// <Home className="h-3.5 w-3.5 mr-1" />
|
------------------------------ */}
|
||||||
// Home
|
<nav className="flex items-center text-sm text-muted-foreground">
|
||||||
// </Button>
|
<Button
|
||||||
// <ChevronRight className="h-3.5 w-3.5 mx-1" />
|
variant="link"
|
||||||
// <Button
|
className="p-0 h-auto font-normal text-muted-foreground"
|
||||||
// variant="link"
|
onClick={() => router.push("/")}>
|
||||||
// className="p-0 h-auto font-normal text-muted-foreground"
|
<Home className="h-3.5 w-3.5 mr-1" />
|
||||||
// onClick={() => router.push("/farms")}>
|
Home
|
||||||
// Farms
|
</Button>
|
||||||
// </Button>
|
<ChevronRight className="h-3.5 w-3.5 mx-1" />
|
||||||
// <ChevronRight className="h-3.5 w-3.5 mx-1" />
|
<Button
|
||||||
// <span className="text-foreground font-medium truncate">{farm?.name || "Farm Details"}</span>
|
variant="link"
|
||||||
// </nav>
|
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
|
Back Navigation Button
|
||||||
// variant="outline"
|
------------------------------ */}
|
||||||
// size="sm"
|
<Button
|
||||||
// className="w-fit gap-2 text-muted-foreground"
|
variant="outline"
|
||||||
// onClick={() => router.push("/farms")}>
|
size="sm"
|
||||||
// <ArrowLeft className="h-4 w-4" /> Back to Farms
|
className="w-fit gap-2 text-muted-foreground"
|
||||||
// </Button>
|
onClick={() => router.push("/farms")}>
|
||||||
|
<ArrowLeft className="h-4 w-4" /> Back to Farms
|
||||||
|
</Button>
|
||||||
|
|
||||||
// {/* Error state */}
|
{/* ------------------------------
|
||||||
// {error && (
|
Error and Loading States
|
||||||
// <Alert variant="destructive">
|
------------------------------ */}
|
||||||
// <AlertTriangle className="h-4 w-4" />
|
{isError && !isLoadingFarm && !farm && (
|
||||||
// <AlertTitle>Error</AlertTitle>
|
<Alert variant="destructive">
|
||||||
// <AlertDescription>{error}</AlertDescription>
|
<AlertTriangle className="h-4 w-4" />
|
||||||
// </Alert>
|
<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 && (
|
Farm Details and Statistics
|
||||||
// <div className="flex flex-col items-center justify-center py-12">
|
------------------------------ */}
|
||||||
// <Loader2 className="h-8 w-8 text-green-600 animate-spin mb-4" />
|
{!isLoadingFarm && !isErrorFarm && farm && (
|
||||||
// <p className="text-muted-foreground">Loading farm details...</p>
|
<>
|
||||||
// </div>
|
<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 */}
|
{/* Weather Overview Card */}
|
||||||
// {!isLoading && !error && farm && (
|
<Card className="md:col-span-4">
|
||||||
// <>
|
<CardHeader>
|
||||||
// <div className="grid gap-6 md:grid-cols-12">
|
<CardTitle className="text-lg font-semibold flex items-center">
|
||||||
// {/* Farm info card */}
|
<Sun className="h-5 w-5 mr-2 text-yellow-500" /> Weather Overview
|
||||||
// <Card className="md:col-span-8">
|
</CardTitle>
|
||||||
// <CardHeader className="pb-2">
|
<CardDescription>Current conditions</CardDescription>
|
||||||
// <div className="flex items-center justify-between">
|
</CardHeader>
|
||||||
// <Badge
|
<CardContent className="space-y-3">
|
||||||
// variant="outline"
|
<div className="flex justify-between items-center">
|
||||||
// className="capitalize bg-green-50 dark:bg-green-900 text-green-700 dark:text-green-300 border-green-200">
|
<span className="text-muted-foreground">Temperature</span>
|
||||||
// {farm.type}
|
<span className="font-medium">25°C</span>
|
||||||
// </Badge>
|
</div>
|
||||||
// <div className="flex items-center text-sm text-muted-foreground">
|
<div className="flex justify-between items-center">
|
||||||
// <Calendar className="h-4 w-4 mr-1" />
|
<span className="text-muted-foreground">Humidity</span>
|
||||||
// Created {farm.createdAt.toLocaleDateString()}
|
<span className="font-medium">60%</span>
|
||||||
// </div>
|
</div>
|
||||||
// </div>
|
<div className="flex justify-between items-center">
|
||||||
// <div className="flex items-start gap-4 mt-2">
|
<span className="text-muted-foreground">Wind</span>
|
||||||
// <div className="h-12 w-12 rounded-full bg-green-100 dark:bg-green-800 flex items-center justify-center">
|
<span className="font-medium">10 km/h</span>
|
||||||
// <Sprout className="h-6 w-6 text-green-600 dark:text-green-300" />
|
</div>
|
||||||
// </div>
|
<div className="flex justify-between items-center">
|
||||||
// <div>
|
<span className="text-muted-foreground">Rainfall (24h)</span>
|
||||||
// <h1 className="text-2xl font-bold">{farm.name}</h1>
|
<span className="font-medium">2 mm</span>
|
||||||
// <div className="flex items-center text-muted-foreground mt-1">
|
</div>
|
||||||
// <MapPin className="h-4 w-4 mr-1" />
|
</CardContent>
|
||||||
// {farm.location}
|
</Card>
|
||||||
// </div>
|
</div>
|
||||||
// </div>
|
|
||||||
// </div>
|
|
||||||
// </CardHeader>
|
|
||||||
// <CardContent>
|
|
||||||
// <div className="grid grid-cols-2 sm:grid-cols-4 gap-4 mt-2">
|
|
||||||
// <div className="bg-muted/30 dark:bg-muted/20 rounded-lg p-3">
|
|
||||||
// <p className="text-xs text-muted-foreground">Total Area</p>
|
|
||||||
// <p className="text-lg font-semibold">{farm.area}</p>
|
|
||||||
// </div>
|
|
||||||
// <div className="bg-muted/30 dark:bg-muted/20 rounded-lg p-3">
|
|
||||||
// <p className="text-xs text-muted-foreground">Total Crops</p>
|
|
||||||
// <p className="text-lg font-semibold">{farm.crops}</p>
|
|
||||||
// </div>
|
|
||||||
// <div className="bg-muted/30 dark:bg-muted/20 rounded-lg p-3">
|
|
||||||
// <p className="text-xs text-muted-foreground">Growing Crops</p>
|
|
||||||
// <p className="text-lg font-semibold">{cropCounts.growing}</p>
|
|
||||||
// </div>
|
|
||||||
// <div className="bg-muted/30 dark:bg-muted/20 rounded-lg p-3">
|
|
||||||
// <p className="text-xs text-muted-foreground">Harvested</p>
|
|
||||||
// <p className="text-lg font-semibold">{cropCounts.harvested}</p>
|
|
||||||
// </div>
|
|
||||||
// </div>
|
|
||||||
// </CardContent>
|
|
||||||
// </Card>
|
|
||||||
|
|
||||||
// {/* Weather card */}
|
{/* ------------------------------
|
||||||
// <Card className="md:col-span-4">
|
Crops Section: List and Filtering Tabs
|
||||||
// <CardHeader>
|
------------------------------ */}
|
||||||
// <CardTitle className="text-lg">Current Conditions</CardTitle>
|
<div className="mt-4">
|
||||||
// <CardDescription>Weather at your farm location</CardDescription>
|
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-4">
|
||||||
// </CardHeader>
|
<div>
|
||||||
// <CardContent className="space-y-4">
|
<h2 className="text-xl font-bold flex items-center">
|
||||||
// <div className="grid grid-cols-2 gap-4">
|
<LayoutGrid className="h-5 w-5 mr-2 text-green-600 dark:text-green-300" />
|
||||||
// <div className="flex items-start gap-2">
|
Crops / Croplands
|
||||||
// <div className="p-2 rounded-lg bg-orange-50 dark:bg-orange-900">
|
</h2>
|
||||||
// <Sun className="h-4 w-4 text-orange-500 dark:text-orange-200" />
|
<p className="text-sm text-muted-foreground">Manage and monitor all croplands in this farm</p>
|
||||||
// </div>
|
</div>
|
||||||
// <div>
|
<Button
|
||||||
// <p className="text-sm font-medium text-muted-foreground">Temperature</p>
|
onClick={() => setIsDialogOpen(true)}
|
||||||
// <p className="text-xl font-semibold">{farm.weather?.temperature}°C</p>
|
className="gap-2 bg-green-600 hover:bg-green-700 w-full sm:w-auto"
|
||||||
// </div>
|
disabled={mutation.isPending}>
|
||||||
// </div>
|
{mutation.isPending ? <Loader2 className="h-4 w-4 animate-spin" /> : <Plus className="h-4 w-4" />}
|
||||||
// <div className="flex items-start gap-2">
|
Add New Crop
|
||||||
// <div className="p-2 rounded-lg bg-blue-50 dark:bg-blue-900">
|
</Button>
|
||||||
// <Droplets className="h-4 w-4 text-blue-500 dark:text-blue-200" />
|
</div>
|
||||||
// </div>
|
{mutation.isError && (
|
||||||
// <div>
|
<Alert variant="destructive" className="mb-4">
|
||||||
// <p className="text-sm font-medium text-muted-foreground">Humidity</p>
|
<AlertTriangle className="h-4 w-4" />
|
||||||
// <p className="text-xl font-semibold">{farm.weather?.humidity}%</p>
|
<AlertTitle>Failed to Add Crop</AlertTitle>
|
||||||
// </div>
|
<AlertDescription>
|
||||||
// </div>
|
{(mutation.error as Error)?.message || "Could not add the crop. Please try again."}
|
||||||
// <div className="flex items-start gap-2">
|
</AlertDescription>
|
||||||
// <div className="p-2 rounded-lg bg-yellow-50 dark:bg-yellow-900">
|
</Alert>
|
||||||
// <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 */}
|
<Tabs value={activeFilter} onValueChange={setActiveFilter} className="mt-6">
|
||||||
// <div className="mt-4">
|
<TabsList>
|
||||||
// <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-4">
|
{availableStatuses.map((status) => (
|
||||||
// <div>
|
<TabsTrigger key={status} value={status} className="capitalize">
|
||||||
// <h2 className="text-xl font-bold flex items-center">
|
{status === "all" ? "All" : status} ({isLoadingCrops ? "..." : cropCounts[status] ?? 0})
|
||||||
// <LayoutGrid className="h-5 w-5 mr-2 text-green-600 dark:text-green-300" />
|
</TabsTrigger>
|
||||||
// Crops
|
))}
|
||||||
// </h2>
|
</TabsList>
|
||||||
// <p className="text-sm text-muted-foreground">Manage and monitor all crops in this farm</p>
|
|
||||||
// </div>
|
|
||||||
// <Button
|
|
||||||
// onClick={() => setIsDialogOpen(true)}
|
|
||||||
// className="gap-2 bg-green-600 hover:bg-green-700 w-full sm:w-auto">
|
|
||||||
// <Plus className="h-4 w-4" />
|
|
||||||
// Add New Crop
|
|
||||||
// </Button>
|
|
||||||
// </div>
|
|
||||||
|
|
||||||
// <Tabs defaultValue="all" className="mt-6">
|
{isLoadingCrops ? (
|
||||||
// <TabsList>
|
<div className="flex justify-center py-12">
|
||||||
// <TabsTrigger value="all" onClick={() => setActiveFilter("all")}>
|
<Loader2 className="h-6 w-6 text-green-600 animate-spin" />
|
||||||
// All Crops ({cropCounts.all})
|
</div>
|
||||||
// </TabsTrigger>
|
) : isErrorCrops ? (
|
||||||
// <TabsTrigger value="growing" onClick={() => setActiveFilter("growing")}>
|
<div className="text-center py-12 text-destructive">Failed to load crops.</div>
|
||||||
// Growing ({cropCounts.growing})
|
) : (
|
||||||
// </TabsTrigger>
|
availableStatuses.map((status) => (
|
||||||
// <TabsTrigger value="planned" onClick={() => setActiveFilter("planned")}>
|
<TabsContent key={status} value={status} className="mt-6">
|
||||||
// Planned ({cropCounts.planned})
|
{filteredCrops.length === 0 && activeFilter === status ? (
|
||||||
// </TabsTrigger>
|
<div className="flex flex-col items-center justify-center py-12 bg-muted/20 dark:bg-muted/30 rounded-lg border border-dashed">
|
||||||
// <TabsTrigger value="harvested" onClick={() => setActiveFilter("harvested")}>
|
<div className="bg-green-100 dark:bg-green-900 p-3 rounded-full mb-4">
|
||||||
// Harvested ({cropCounts.harvested})
|
<Sprout className="h-6 w-6 text-green-600 dark:text-green-300" />
|
||||||
// </TabsTrigger>
|
</div>
|
||||||
// </TabsList>
|
<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 ? (
|
Add Crop Dialog Component
|
||||||
// <div className="flex flex-col items-center justify-center py-12 bg-muted/20 dark:bg-muted/30 rounded-lg border border-dashed">
|
- Passes the mutation state to display loading indicators.
|
||||||
// <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" />
|
<CropDialog
|
||||||
// </div>
|
open={isDialogOpen}
|
||||||
// <h3 className="text-xl font-medium mb-2">No crops found</h3>
|
onOpenChange={setIsDialogOpen}
|
||||||
// <p className="text-muted-foreground text-center max-w-md mb-6">
|
onSubmit={handleAddCrop}
|
||||||
// {activeFilter === "all"
|
isSubmitting={mutation.isPending}
|
||||||
// ? "You haven't added any crops to this farm yet."
|
/>
|
||||||
// : `No ${activeFilter} crops found. Try a different filter.`}
|
</div>
|
||||||
// </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>;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -71,11 +71,11 @@ export function AddFarmForm({ onSubmit, onCancel }: AddFarmFormProps) {
|
|||||||
try {
|
try {
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
const farmData: Partial<Farm> = {
|
const farmData: Partial<Farm> = {
|
||||||
Name: values.name,
|
name: values.name,
|
||||||
Lat: values.latitude,
|
lat: values.latitude,
|
||||||
Lon: values.longitude,
|
lon: values.longitude,
|
||||||
FarmType: values.type,
|
farmType: values.type,
|
||||||
TotalSize: values.area,
|
totalSize: values.area,
|
||||||
};
|
};
|
||||||
await onSubmit(farmData);
|
await onSubmit(farmData);
|
||||||
form.reset();
|
form.reset();
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import type { Farm } from "@/types";
|
|||||||
|
|
||||||
export interface FarmCardProps {
|
export interface FarmCardProps {
|
||||||
variant: "farm" | "add";
|
variant: "farm" | "add";
|
||||||
farm?: Farm;
|
farm?: Farm; // Use updated Farm type
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,7 +40,7 @@ export function FarmCard({ variant, farm, onClick }: FarmCardProps) {
|
|||||||
year: "numeric",
|
year: "numeric",
|
||||||
month: "short",
|
month: "short",
|
||||||
day: "numeric",
|
day: "numeric",
|
||||||
}).format(new Date(farm.CreatedAt));
|
}).format(new Date(farm.createdAt));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className={cardClasses} onClick={onClick}>
|
<Card className={cardClasses} onClick={onClick}>
|
||||||
@ -49,7 +49,7 @@ export function FarmCard({ variant, farm, onClick }: FarmCardProps) {
|
|||||||
<Badge
|
<Badge
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="capitalize bg-green-50 dark:bg-green-900 text-green-700 dark:text-green-300 border-green-200">
|
className="capitalize bg-green-50 dark:bg-green-900 text-green-700 dark:text-green-300 border-green-200">
|
||||||
{farm.FarmType}
|
{farm.farmType}
|
||||||
</Badge>
|
</Badge>
|
||||||
<div className="flex items-center text-xs text-muted-foreground">
|
<div className="flex items-center text-xs text-muted-foreground">
|
||||||
<CalendarDays className="h-3 w-3 mr-1" />
|
<CalendarDays className="h-3 w-3 mr-1" />
|
||||||
@ -63,19 +63,19 @@ export function FarmCard({ variant, farm, onClick }: FarmCardProps) {
|
|||||||
<Sprout className="h-5 w-5 text-green-600" />
|
<Sprout className="h-5 w-5 text-green-600" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-xl font-medium mb-1 truncate">{farm.Name}</h3>
|
<h3 className="text-xl font-medium mb-1 truncate">{farm.name}</h3>
|
||||||
<div className="flex items-center text-sm text-muted-foreground mb-2">
|
<div className="flex items-center text-sm text-muted-foreground mb-2">
|
||||||
<MapPin className="h-3.5 w-3.5 mr-1 flex-shrink-0" />
|
<MapPin className="h-3.5 w-3.5 mr-1 flex-shrink-0" />
|
||||||
<span className="truncate">{farm.Lat}</span>
|
<span className="truncate">{farm.lat}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-2 mt-3">
|
<div className="grid grid-cols-2 gap-2 mt-3">
|
||||||
<div className="bg-muted/30 dark:bg-muted/20 rounded-md p-2 text-center">
|
<div className="bg-muted/30 dark:bg-muted/20 rounded-md p-2 text-center">
|
||||||
<p className="text-xs text-muted-foreground">Area</p>
|
<p className="text-xs text-muted-foreground">Area</p>
|
||||||
<p className="font-medium">{farm.TotalSize}</p>
|
<p className="font-medium">{farm.totalSize}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-muted/30 dark:bg-muted/20 rounded-md p-2 text-center">
|
<div className="bg-muted/30 dark:bg-muted/20 rounded-md p-2 text-center">
|
||||||
<p className="text-xs text-muted-foreground">Crops</p>
|
<p className="text-xs text-muted-foreground">Crops</p>
|
||||||
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -36,18 +36,21 @@ export default function FarmSetupPage() {
|
|||||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: farms,
|
data: farms, // Type is Farm[] now
|
||||||
isLoading,
|
isLoading,
|
||||||
isError,
|
isError,
|
||||||
error,
|
error,
|
||||||
} = useQuery<Farm[]>({
|
} = useQuery<Farm[]>({
|
||||||
|
// Use Farm[] type
|
||||||
queryKey: ["farms"],
|
queryKey: ["farms"],
|
||||||
queryFn: fetchFarms,
|
queryFn: fetchFarms,
|
||||||
staleTime: 60 * 1000,
|
staleTime: 60 * 1000,
|
||||||
});
|
});
|
||||||
|
|
||||||
const mutation = useMutation({
|
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: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ["farms"] });
|
queryClient.invalidateQueries({ queryKey: ["farms"] });
|
||||||
setIsDialogOpen(false);
|
setIsDialogOpen(false);
|
||||||
@ -69,23 +72,23 @@ export default function FarmSetupPage() {
|
|||||||
const filteredAndSortedFarms = (farms || [])
|
const filteredAndSortedFarms = (farms || [])
|
||||||
.filter(
|
.filter(
|
||||||
(farm) =>
|
(farm) =>
|
||||||
(activeFilter === "all" || farm.FarmType === activeFilter) &&
|
(activeFilter === "all" || farm.farmType === activeFilter) && // Use camelCase farmType
|
||||||
(farm.Name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
(farm.name.toLowerCase().includes(searchQuery.toLowerCase()) || // Use camelCase name
|
||||||
// farm.location.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
// farm.location is no longer a single string, use lat/lon if needed for search
|
||||||
farm.FarmType.toLowerCase().includes(searchQuery.toLowerCase()))
|
farm.farmType.toLowerCase().includes(searchQuery.toLowerCase())) // Use camelCase farmType
|
||||||
)
|
)
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
if (sortOrder === "newest") {
|
if (sortOrder === "newest") {
|
||||||
return new Date(b.CreatedAt).getTime() - new Date(a.CreatedAt).getTime();
|
return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(); // Use camelCase createdAt
|
||||||
} else if (sortOrder === "oldest") {
|
} else if (sortOrder === "oldest") {
|
||||||
return new Date(a.CreatedAt).getTime() - new Date(b.CreatedAt).getTime();
|
return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(); // Use camelCase createdAt
|
||||||
} else {
|
} else {
|
||||||
return a.Name.localeCompare(b.Name);
|
return a.name.localeCompare(b.name); // Use camelCase name
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get distinct farm types.
|
// 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>) => {
|
const handleAddFarm = async (data: Partial<Farm>) => {
|
||||||
await mutation.mutateAsync(data);
|
await mutation.mutateAsync(data);
|
||||||
@ -133,6 +136,7 @@ export default function FarmSetupPage() {
|
|||||||
</Badge>
|
</Badge>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
{/* DropdownMenu remains the same, Check icon was missing */}
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button variant="outline" className="gap-2">
|
<Button variant="outline" className="gap-2">
|
||||||
@ -228,23 +232,17 @@ export default function FarmSetupPage() {
|
|||||||
{!isLoading && !isError && filteredAndSortedFarms.length > 0 && (
|
{!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">
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
<motion.div
|
<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">
|
|
||||||
<FarmCard variant="add" onClick={() => setIsDialogOpen(true)} />
|
<FarmCard variant="add" onClick={() => setIsDialogOpen(true)} />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
{filteredAndSortedFarms.map((farm, index) => (
|
{filteredAndSortedFarms.map((farm, index) => (
|
||||||
<motion.div
|
<motion.div
|
||||||
key={farm.UUID}
|
key={farm.uuid} // Use camelCase uuid initial={{ opacity: 0, y: 20 }}
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
exit={{ opacity: 0, y: -20 }}
|
exit={{ opacity: 0, y: -20 }}
|
||||||
transition={{ duration: 0.2, delay: index * 0.05 }}
|
transition={{ duration: 0.2, delay: index * 0.05 }}
|
||||||
className="col-span-1">
|
className="col-span-1">
|
||||||
<FarmCard variant="farm" farm={farm} onClick={() => router.push(`/farms/${farm.UUID}`)} />
|
<FarmCard variant="farm" farm={farm} onClick={() => router.push(`/farms/${farm.uuid}`)} />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
))}
|
))}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
@ -260,6 +258,7 @@ export default function FarmSetupPage() {
|
|||||||
<DialogTitle>Add New Farm</DialogTitle>
|
<DialogTitle>Add New Farm</DialogTitle>
|
||||||
<DialogDescription>Fill out the details below to add a new farm to your account.</DialogDescription>
|
<DialogDescription>Fill out the details below to add a new farm to your account.</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
{/* Pass handleAddFarm (which now expects Partial<Farm>) */}
|
||||||
<AddFarmForm onSubmit={handleAddFarm} onCancel={() => setIsDialogOpen(false)} />
|
<AddFarmForm onSubmit={handleAddFarm} onCancel={() => setIsDialogOpen(false)} />
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@ -107,10 +107,11 @@ export function AppSidebar({ config, ...props }: AppSidebarProps) {
|
|||||||
async function getUser() {
|
async function getUser() {
|
||||||
try {
|
try {
|
||||||
const data = await fetchUserMe();
|
const data = await fetchUserMe();
|
||||||
|
console.log(data);
|
||||||
setUser({
|
setUser({
|
||||||
name: data.user.UUID,
|
name: data.user.uuid,
|
||||||
email: data.user.Email,
|
email: data.user.email,
|
||||||
avatar: data.user.Avatar || "/avatars/avatar.webp",
|
avatar: data.user.avatar || "/avatars/avatar.webp",
|
||||||
});
|
});
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
if (err instanceof Error) {
|
if (err instanceof Error) {
|
||||||
|
|||||||
@ -31,6 +31,7 @@
|
|||||||
"@react-oauth/google": "^0.12.1",
|
"@react-oauth/google": "^0.12.1",
|
||||||
"@tailwindcss/typography": "^0.5.16",
|
"@tailwindcss/typography": "^0.5.16",
|
||||||
"@tanstack/react-query": "^5.66.0",
|
"@tanstack/react-query": "^5.66.0",
|
||||||
|
"@tanstack/react-table": "^8.21.2",
|
||||||
"@vis.gl/react-google-maps": "^1.5.2",
|
"@vis.gl/react-google-maps": "^1.5.2",
|
||||||
"axios": "^1.7.9",
|
"axios": "^1.7.9",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
|
|||||||
@ -74,6 +74,9 @@ importers:
|
|||||||
'@tanstack/react-query':
|
'@tanstack/react-query':
|
||||||
specifier: ^5.66.0
|
specifier: ^5.66.0
|
||||||
version: 5.67.3(react@19.0.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':
|
'@vis.gl/react-google-maps':
|
||||||
specifier: ^1.5.2
|
specifier: ^1.5.2
|
||||||
version: 1.5.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
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:
|
react-hook-form:
|
||||||
specifier: ^7.54.2
|
specifier: ^7.54.2
|
||||||
version: 7.54.2(react@19.0.0)
|
version: 7.54.2(react@19.0.0)
|
||||||
|
react-icons:
|
||||||
|
specifier: ^5.5.0
|
||||||
|
version: 5.5.0(react@19.0.0)
|
||||||
recharts:
|
recharts:
|
||||||
specifier: ^2.15.1
|
specifier: ^2.15.1
|
||||||
version: 2.15.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
version: 2.15.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||||
@ -974,6 +980,17 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^18 || ^19
|
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':
|
'@types/d3-array@3.2.1':
|
||||||
resolution: {integrity: sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==}
|
resolution: {integrity: sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==}
|
||||||
|
|
||||||
@ -2294,6 +2311,11 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^16.8.0 || ^17 || ^18 || ^19
|
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:
|
react-is@16.13.1:
|
||||||
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
||||||
|
|
||||||
@ -3500,6 +3522,14 @@ snapshots:
|
|||||||
'@tanstack/query-core': 5.67.3
|
'@tanstack/query-core': 5.67.3
|
||||||
react: 19.0.0
|
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-array@3.2.1': {}
|
||||||
|
|
||||||
'@types/d3-color@3.1.3': {}
|
'@types/d3-color@3.1.3': {}
|
||||||
@ -4977,6 +5007,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
react: 19.0.0
|
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@16.13.1: {}
|
||||||
|
|
||||||
react-is@18.3.1: {}
|
react-is@18.3.1: {}
|
||||||
|
|||||||
@ -1,131 +1,156 @@
|
|||||||
export interface Plant {
|
export interface GeoPosition {
|
||||||
UUID: string;
|
lat: number;
|
||||||
Name: string;
|
lng: number;
|
||||||
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 Crop {
|
export interface GeoMarker {
|
||||||
id: string;
|
type: "marker";
|
||||||
farmId: string;
|
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;
|
name: string;
|
||||||
plantedDate: Date;
|
|
||||||
expectedHarvest?: Date;
|
|
||||||
status: string;
|
|
||||||
variety?: string;
|
variety?: string;
|
||||||
area?: string;
|
averageHeight?: number;
|
||||||
healthScore?: number;
|
daysToEmerge?: number;
|
||||||
progress?: 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 {
|
export interface Cropland {
|
||||||
UUID: string;
|
uuid: string;
|
||||||
Name: string;
|
name: string;
|
||||||
Status: string;
|
status: string;
|
||||||
Priority: number;
|
priority: number;
|
||||||
LandSize: number;
|
landSize: number;
|
||||||
GrowthStage: string;
|
growthStage: string;
|
||||||
PlantID: string;
|
plantId: string;
|
||||||
FarmID: string;
|
farmId: string;
|
||||||
CreatedAt: Date;
|
geoFeature?: GeoFeatureData | null;
|
||||||
UpdatedAt: Date;
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CropAnalytics {
|
export interface CropAnalytics {
|
||||||
cropId: string;
|
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;
|
growthProgress: number;
|
||||||
humidity: number;
|
plantHealth?: "good" | "warning" | "critical";
|
||||||
temperature: number;
|
nextAction?: string | null;
|
||||||
sunlight: number;
|
nextActionDue?: string | null;
|
||||||
waterLevel: number;
|
nutrientLevels?: {
|
||||||
plantHealth: "good" | "warning" | "critical";
|
nitrogen: number | null;
|
||||||
nextAction: string;
|
phosphorus: number | null;
|
||||||
nextActionDue: Date;
|
potassium: number | null;
|
||||||
soilMoisture: number;
|
} | null;
|
||||||
windSpeed: string;
|
|
||||||
rainfall: string;
|
|
||||||
nutrientLevels: {
|
|
||||||
nitrogen: number;
|
|
||||||
phosphorus: number;
|
|
||||||
potassium: number;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Farm {
|
export interface Farm {
|
||||||
CreatedAt: Date;
|
uuid: string;
|
||||||
FarmType: string;
|
name: string;
|
||||||
Lat: number;
|
farmType: string;
|
||||||
Lon: number;
|
lat: number;
|
||||||
Name: string;
|
lon: number;
|
||||||
OwnerID: string;
|
ownerId: string;
|
||||||
TotalSize: string;
|
totalSize: string;
|
||||||
UUID: string;
|
createdAt: Date;
|
||||||
UpdatedAt: Date;
|
updatedAt: Date;
|
||||||
Crops: Cropland[];
|
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 {
|
export interface User {
|
||||||
ID: number;
|
id: number;
|
||||||
UUID: string;
|
uuid: string;
|
||||||
Username: string;
|
username?: string;
|
||||||
Password: string;
|
password?: string;
|
||||||
Email: string;
|
email: string;
|
||||||
CreatedAt: string;
|
createdAt: string;
|
||||||
UpdatedAt: string;
|
updatedAt: string;
|
||||||
Avatar: string;
|
avatar?: string;
|
||||||
IsActive: boolean;
|
isActive: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type InventoryItem = {
|
export interface InventoryItem {
|
||||||
id: number;
|
id: string;
|
||||||
|
userId: string;
|
||||||
name: string;
|
name: string;
|
||||||
category: string;
|
categoryId: number;
|
||||||
type: string;
|
category: { id: number; name: string };
|
||||||
quantity: number;
|
quantity: number;
|
||||||
unit: string;
|
unitId: number;
|
||||||
lastUpdated: string;
|
unit: { id: number; name: string };
|
||||||
status: string;
|
dateAdded: string;
|
||||||
};
|
statusId: number;
|
||||||
export type InventoryItemStatus = {
|
status: { id: number; name: string };
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InventoryStatus {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
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 {
|
export interface Blog {
|
||||||
id: number;
|
id: number;
|
||||||
@ -152,8 +177,6 @@ export interface Blog {
|
|||||||
}[];
|
}[];
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------- Maps -----------$
|
|
||||||
|
|
||||||
export type OverlayGeometry =
|
export type OverlayGeometry =
|
||||||
| google.maps.Marker
|
| google.maps.Marker
|
||||||
| google.maps.Polygon
|
| google.maps.Polygon
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user