Merge branch 'main' into feature-authen

This commit is contained in:
Sosokker 2025-02-14 00:15:03 +07:00
commit e52c003470
15 changed files with 7059 additions and 4840 deletions

View File

@ -0,0 +1,36 @@
package domain
import (
"context"
"time"
validation "github.com/go-ozzo/ozzo-validation/v4"
)
type Cropland struct {
UUID string
Name string
Status string
Priority int
LandSize float64
GrowthStage string
PlantID string
FarmID string
CreatedAt time.Time
UpdatedAt time.Time
}
func (c *Cropland) Validate() error {
return validation.ValidateStruct(c,
validation.Field(&c.Name, validation.Required),
validation.Field(&c.Status, validation.Required),
validation.Field(&c.GrowthStage, validation.Required),
validation.Field(&c.LandSize, validation.Required),
)
}
type CroplandRepository interface {
GetByID(context.Context, string) (Cropland, error)
CreateOrUpdate(context.Context, *Cropland) error
Delete(context.Context, string) error
}

View File

@ -0,0 +1,33 @@
package domain
import (
"context"
"time"
validation "github.com/go-ozzo/ozzo-validation/v4"
)
type Farm struct {
UUID string
Name string
Lat float64
Lon float64
CreatedAt time.Time
UpdatedAt time.Time
OwnerID string
}
func (f *Farm) Validate() error {
return validation.ValidateStruct(f,
validation.Field(&f.Name, validation.Required),
validation.Field(&f.Lat, validation.Required),
validation.Field(&f.Lon, validation.Required),
validation.Field(&f.OwnerID, validation.Required),
)
}
type FarmRepository interface {
GetByID(context.Context, string) (Farm, error)
CreateOrUpdate(context.Context, *Farm) error
Delete(context.Context, string) error
}

View File

@ -0,0 +1,111 @@
package repository
import (
"context"
"strings"
"github.com/google/uuid"
"github.com/forfarm/backend/internal/domain"
)
type postgresCroplandRepository struct {
conn Connection
}
func NewPostgresCropland(conn Connection) domain.CroplandRepository {
return &postgresCroplandRepository{conn: conn}
}
func (p *postgresCroplandRepository) fetch(ctx context.Context, query string, args ...interface{}) ([]domain.Cropland, error) {
rows, err := p.conn.Query(ctx, query, args...)
if err != nil {
return nil, err
}
defer rows.Close()
var croplands []domain.Cropland
for rows.Next() {
var c domain.Cropland
if err := rows.Scan(
&c.UUID,
&c.Name,
&c.Status,
&c.Priority,
&c.LandSize,
&c.GrowthStage,
&c.PlantID,
&c.FarmID,
&c.CreatedAt,
&c.UpdatedAt,
); err != nil {
return nil, err
}
croplands = append(croplands, c)
}
return croplands, nil
}
func (p *postgresCroplandRepository) GetByID(ctx context.Context, uuid string) (domain.Cropland, error) {
query := `
SELECT uuid, name, status, priority, land_size, growth_stage, plant_id, farm_id, created_at, updated_at
FROM croplands
WHERE uuid = $1`
croplands, err := p.fetch(ctx, query, uuid)
if err != nil {
return domain.Cropland{}, err
}
if len(croplands) == 0 {
return domain.Cropland{}, domain.ErrNotFound
}
return croplands[0], nil
}
func (p *postgresCroplandRepository) GetByFarmID(ctx context.Context, farmID string) ([]domain.Cropland, error) {
query := `
SELECT uuid, name, status, priority, land_size, growth_stage, plant_id, farm_id, created_at, updated_at
FROM croplands
WHERE farm_id = $1`
return p.fetch(ctx, query, farmID)
}
func (p *postgresCroplandRepository) CreateOrUpdate(ctx context.Context, c *domain.Cropland) error {
if strings.TrimSpace(c.UUID) == "" {
c.UUID = uuid.New().String()
}
query := `
INSERT INTO croplands (uuid, name, status, priority, land_size, growth_stage, plant_id, farm_id, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, NOW(), NOW())
ON CONFLICT (uuid) DO UPDATE
SET name = EXCLUDED.name,
status = EXCLUDED.status,
priority = EXCLUDED.priority,
land_size = EXCLUDED.land_size,
growth_stage = EXCLUDED.growth_stage,
plant_id = EXCLUDED.plant_id,
farm_id = EXCLUDED.farm_id,
updated_at = NOW()
RETURNING uuid, created_at, updated_at`
return p.conn.QueryRow(
ctx,
query,
c.UUID,
c.Name,
c.Status,
c.Priority,
c.LandSize,
c.GrowthStage,
c.PlantID,
c.FarmID,
).Scan(&c.CreatedAt, &c.UpdatedAt)
}
func (p *postgresCroplandRepository) Delete(ctx context.Context, uuid string) error {
query := `DELETE FROM croplands WHERE uuid = $1`
_, err := p.conn.Exec(ctx, query, uuid)
return err
}

View File

@ -0,0 +1,102 @@
package repository
import (
"context"
"strings"
"github.com/google/uuid"
"github.com/forfarm/backend/internal/domain"
)
type postgresFarmRepository struct {
conn Connection
}
func NewPostgresFarm(conn Connection) domain.FarmRepository {
return &postgresFarmRepository{conn: conn}
}
func (p *postgresFarmRepository) fetch(ctx context.Context, query string, args ...interface{}) ([]domain.Farm, error) {
rows, err := p.conn.Query(ctx, query, args...)
if err != nil {
return nil, err
}
defer rows.Close()
var farms []domain.Farm
for rows.Next() {
var f domain.Farm
if err := rows.Scan(
&f.UUID,
&f.Name,
&f.Lat,
&f.Lon,
&f.CreatedAt,
&f.UpdatedAt,
&f.OwnerID,
); err != nil {
return nil, err
}
farms = append(farms, f)
}
return farms, nil
}
func (p *postgresFarmRepository) GetByID(ctx context.Context, uuid string) (domain.Farm, error) {
query := `
SELECT uuid, name, lat, lon, created_at, updated_at, owner_id
FROM farms
WHERE uuid = $1`
farms, err := p.fetch(ctx, query, uuid)
if err != nil {
return domain.Farm{}, err
}
if len(farms) == 0 {
return domain.Farm{}, domain.ErrNotFound
}
return farms[0], nil
}
func (p *postgresFarmRepository) GetByOwnerID(ctx context.Context, ownerID string) ([]domain.Farm, error) {
query := `
SELECT uuid, name, lat, lon, created_at, updated_at, owner_id
FROM farms
WHERE owner_id = $1`
return p.fetch(ctx, query, ownerID)
}
func (p *postgresFarmRepository) CreateOrUpdate(ctx context.Context, f *domain.Farm) error {
if strings.TrimSpace(f.UUID) == "" {
f.UUID = uuid.New().String()
}
query := `
INSERT INTO farms (uuid, name, lat, lon, created_at, updated_at, owner_id)
VALUES ($1, $2, $3, $4, NOW(), NOW(), $5)
ON CONFLICT (uuid) DO UPDATE
SET name = EXCLUDED.name,
lat = EXCLUDED.lat,
lon = EXCLUDED.lon,
updated_at = NOW(),
owner_id = EXCLUDED.owner_id
RETURNING uuid, created_at, updated_at`
return p.conn.QueryRow(
ctx,
query,
f.UUID,
f.Name,
f.Lat,
f.Lon,
f.OwnerID,
).Scan(&f.CreatedAt, &f.UpdatedAt)
}
func (p *postgresFarmRepository) Delete(ctx context.Context, uuid string) error {
query := `DELETE FROM farms WHERE uuid = $1`
_, err := p.conn.Exec(ctx, query, uuid)
return err
}

View File

@ -0,0 +1,67 @@
-- +goose Up
CREATE TABLE light_profiles (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL UNIQUE
);
CREATE TABLE soil_conditions (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL UNIQUE
);
CREATE TABLE harvest_units (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL UNIQUE
);
CREATE TABLE plants (
uuid UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
variety TEXT,
row_spacing DOUBLE PRECISION,
optimal_temp DOUBLE PRECISION,
planting_depth DOUBLE PRECISION,
average_height DOUBLE PRECISION,
light_profile_id INT NOT NULL,
soil_condition_id INT NOT NULL,
planting_detail TEXT,
is_perennial BOOLEAN NOT NULL DEFAULT FALSE,
days_to_emerge INT,
days_to_flower INT,
days_to_maturity INT,
harvest_window INT,
ph_value DOUBLE PRECISION,
estimate_loss_rate DOUBLE PRECISION,
estimate_revenue_per_hu DOUBLE PRECISION,
harvest_unit_id INT NOT NULL,
water_needs DOUBLE PRECISION,
CONSTRAINT fk_plant_light_profile FOREIGN KEY (light_profile_id) REFERENCES light_profiles(id),
CONSTRAINT fk_plant_soil_condition FOREIGN KEY (soil_condition_id) REFERENCES soil_conditions(id),
CONSTRAINT fk_plant_harvest_unit FOREIGN KEY (harvest_unit_id) REFERENCES harvest_units(id)
);
CREATE TABLE farms (
uuid UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
lat DOUBLE PRECISION NOT NULL,
lon DOUBLE PRECISION NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
owner_id UUID NOT NULL,
CONSTRAINT fk_farm_owner FOREIGN KEY (owner_id) REFERENCES users(uuid) ON DELETE CASCADE
);
CREATE TABLE croplands (
uuid UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
status TEXT NOT NULL,
priority INT NOT NULL,
land_size DOUBLE PRECISION NOT NULL,
growth_stage TEXT NOT NULL,
plant_id UUID NOT NULL,
farm_id UUID NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT fk_cropland_farm FOREIGN KEY (farm_id) REFERENCES farms(uuid) ON DELETE CASCADE,
CONSTRAINT fk_cropland_plant FOREIGN KEY (plant_id) REFERENCES plants(uuid) ON DELETE CASCADE
);

View File

@ -0,0 +1,67 @@
"use client";
import { GoogleMap, LoadScript, DrawingManager } from "@react-google-maps/api";
import { useState, useCallback } from "react";
const containerStyle = {
width: "100%",
height: "500px",
};
const center = { lat: 13.7563, lng: 100.5018 }; // Example: Bangkok, Thailand
const GoogleMapWithDrawing = () => {
const [map, setMap] = useState<google.maps.Map | null>(null);
// Handles drawing complete
const onDrawingComplete = useCallback(
(overlay: google.maps.drawing.OverlayCompleteEvent) => {
console.log("Drawing complete:", overlay);
},
[]
);
return (
<LoadScript
googleMapsApiKey={process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY!}
libraries={["drawing"]}
>
<GoogleMap
mapContainerStyle={containerStyle}
center={center}
zoom={10}
onLoad={(map) => setMap(map)}
>
{map && (
<DrawingManager
onOverlayComplete={onDrawingComplete}
options={{
drawingControl: true,
drawingControlOptions: {
position: google.maps.ControlPosition.TOP_CENTER,
drawingModes: [
google.maps.drawing.OverlayType.POLYGON,
google.maps.drawing.OverlayType.RECTANGLE,
google.maps.drawing.OverlayType.CIRCLE,
google.maps.drawing.OverlayType.POLYLINE,
],
},
polygonOptions: {
fillColor: "#FF0000",
fillOpacity: 0.5,
strokeWeight: 2,
},
rectangleOptions: {
fillColor: "#00FF00",
fillOpacity: 0.5,
strokeWeight: 2,
},
}}
/>
)}
</GoogleMap>
</LoadScript>
);
};
export default GoogleMapWithDrawing;

View File

@ -0,0 +1,230 @@
"use client";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { harvestDetailsFormSchema } from "@/schemas/application.schema";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
type harvestSchema = z.infer<typeof harvestDetailsFormSchema>;
export default function HarvestDetailsForm() {
const form = useForm<harvestSchema>({
resolver: zodResolver(harvestDetailsFormSchema),
defaultValues: {},
});
return (
<Form {...form}>
<form className="grid grid-cols-3 gap-5">
<FormField
control={form.control}
name="daysToFlower"
render={({ field }: { field: any }) => (
<FormItem>
<FormLabel className="font-bold text-lg">
Days To Flower
</FormLabel>
<FormControl>
<div className="mt-5 space-y-5">
<div className="flex space-x-5">
<Input
type="number"
id="daysToFlower"
className="w-96"
{...field}
/>
</div>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="daysToMaturity"
render={({ field }: { field: any }) => (
<FormItem>
<FormLabel className="font-bold text-lg">
Days To Maturity
</FormLabel>
<FormControl>
<div className="mt-5 space-y-5">
<div className="flex space-x-5">
<Input
type="number"
id="daysToMaturity"
className="w-96"
{...field}
/>
</div>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="harvestWindow"
render={({ field }: { field: any }) => (
<FormItem>
<FormLabel className="font-bold text-lg">
Harvest Window
</FormLabel>
<FormControl>
<div className="mt-5 space-y-5">
<div className="flex space-x-5">
<Input
type="number"
id="harvestWindow"
className="w-96"
{...field}
/>
</div>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="estimatedLossRate"
render={({ field }: { field: any }) => (
<FormItem>
<FormLabel className="font-bold text-lg">
Estimated Loss Rate
</FormLabel>
<FormControl>
<div className="mt-5 space-y-5">
<div className="flex space-x-5">
<Input
type="number"
id="estimatedLossRate"
className="w-96"
{...field}
/>
</div>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="harvestUnits"
render={({ field }) => (
<FormItem>
<FormLabel className="font-bold text-lg">Harvest Units</FormLabel>
<FormControl>
<Select
onValueChange={field.onChange}
defaultValue={field.value}
>
<SelectTrigger className="w-96">
<SelectValue placeholder="Select a harvest unit" />
</SelectTrigger>
<SelectContent>
<SelectItem value="bales">bales</SelectItem>
<SelectItem value="transplant">Transplant</SelectItem>
<SelectItem value="cutting">Cutting</SelectItem>
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="estimatedRevenue"
render={({ field }: { field: any }) => (
<FormItem>
<FormLabel className="font-bold text-lg">
Estimated Revenue
</FormLabel>
<FormControl>
<div className="mt-5 space-y-5">
<div className="flex space-x-5">
<Input
type="number"
id="estimatedRevenue"
className="w-96"
{...field}
/>
</div>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="expectedYieldPer100ft"
render={({ field }: { field: any }) => (
<FormItem>
<FormLabel className="font-bold text-lg">
Expected Yield Per100ft
</FormLabel>
<FormControl>
<div className="mt-5 space-y-5">
<div className="flex space-x-5">
<Input
type="number"
id="expectedYieldPer100ft"
className="w-96"
{...field}
/>
</div>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="expectedYieldPerAcre"
render={({ field }: { field: any }) => (
<FormItem>
<FormLabel className="font-bold text-lg">
Expected Yield Per Acre
</FormLabel>
<FormControl>
<div className="mt-5 space-y-5">
<div className="flex space-x-5">
<Input
type="number"
id="expectedYieldPerAcre"
className="w-96"
{...field}
/>
</div>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
);
}

View File

@ -1,7 +1,34 @@
import PlantingDetailsForm from "./planting-detail-form";
import HarvestDetailsForm from "./harvest-detail-form";
import { Separator } from "@/components/ui/separator";
import GoogleMapWithDrawing from "./google-map-with-drawing";
export default function SetupPage() { export default function SetupPage() {
return ( return (
<div> <div className="p-5">
<h1>Setup Page</h1> <div className=" flex justify-center">
<h1 className="flex text-2xl ">Plating Details</h1>
</div>
<Separator className="mt-3" />
<div className="mt-10 flex justify-center">
<PlantingDetailsForm />
</div>
<div className=" flex justify-center mt-20">
<h1 className="flex text-2xl ">Harvest Details</h1>
</div>
<Separator className="mt-3" />
<div className="mt-10 flex justify-center">
<HarvestDetailsForm />
</div>
<div className="mt-10">
<div className=" flex justify-center mt-20">
<h1 className="flex text-2xl ">Map</h1>
</div>
<Separator className="mt-3" />
<div className="mt-10">
<GoogleMapWithDrawing />
</div>
</div>
</div> </div>
); );
} }

View File

@ -0,0 +1,314 @@
"use client";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { plantingDetailsFormSchema } from "@/schemas/application.schema";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
import { Switch } from "@/components/ui/switch";
type plantingSchema = z.infer<typeof plantingDetailsFormSchema>;
export default function PlantingDetailsForm() {
const form = useForm<plantingSchema>({
resolver: zodResolver(plantingDetailsFormSchema),
defaultValues: {},
});
return (
<Form {...form}>
<form className="grid grid-cols-3 gap-5">
<FormField
control={form.control}
name="daysToEmerge"
render={({ field }: { field: any }) => (
<FormItem>
<FormLabel className="font-bold text-lg">Day to Emerge</FormLabel>
<FormControl>
<div className="mt-5 space-y-5">
<div className="flex space-x-5">
<Input
type="number"
id="daysToEmerge"
className="w-96"
{...field}
/>
</div>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="plantSpacing"
render={({ field }: { field: any }) => (
<FormItem>
<FormLabel className="font-bold text-lg">Plant Spacing</FormLabel>
<FormControl>
<div className="mt-5 space-y-5">
<div className="flex space-x-5">
<Input
type="number"
id="plantSpacing"
className="w-96"
{...field}
/>
</div>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="rowSpacing"
render={({ field }: { field: any }) => (
<FormItem>
<FormLabel className="font-bold text-lg">Row Spacing</FormLabel>
<FormControl>
<div className="mt-10 space-y-5">
<div className="flex space-x-5">
<Input
type="number"
id="rowSpacing"
className="w-96"
{...field}
/>
</div>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="plantingDepth"
render={({ field }: { field: any }) => (
<FormItem>
<FormLabel className="font-bold text-lg">
Planting Depth
</FormLabel>
<FormControl>
<div className="mt-10 space-y-5">
<div className="flex space-x-5">
<Input
type="number"
id="plantingDepth"
className="w-96"
{...field}
/>
</div>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="averageHeight"
render={({ field }: { field: any }) => (
<FormItem>
<FormLabel className="font-bold text-lg">
Average Height
</FormLabel>
<FormControl>
<div className="mt-10 space-y-5">
<div className="flex space-x-5">
<Input
type="number"
id="averageHeight"
className="w-96"
{...field}
/>
</div>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="startMethod"
render={({ field }) => (
<FormItem>
<FormLabel className="font-bold text-lg">Start Method</FormLabel>
<FormControl>
<Select
onValueChange={field.onChange}
defaultValue={field.value}
>
<SelectTrigger className="w-96">
<SelectValue placeholder="Select a start method" />
</SelectTrigger>
<SelectContent>
<SelectItem value="seed">Seed</SelectItem>
<SelectItem value="transplant">Transplant</SelectItem>
<SelectItem value="cutting">Cutting</SelectItem>
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="lightProfile"
render={({ field }) => (
<FormItem>
<FormLabel className="font-bold text-lg">Light Profile</FormLabel>
<FormControl>
<Select
onValueChange={field.onChange}
defaultValue={field.value}
>
<SelectTrigger className="w-96">
<SelectValue placeholder="Select light profile" />
</SelectTrigger>
<SelectContent>
<SelectItem value="xp">Seed</SelectItem>
<SelectItem value="xa">Transplant</SelectItem>
<SelectItem value="xz">Cutting</SelectItem>
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="soilConditions"
render={({ field }) => (
<FormItem>
<FormLabel className="font-bold text-lg">
Soil Conditions
</FormLabel>
<FormControl>
<Select
onValueChange={field.onChange}
defaultValue={field.value}
>
<SelectTrigger className="w-96">
<SelectValue placeholder="Select a soil condition" />
</SelectTrigger>
<SelectContent>
<SelectItem value="xp">Seed</SelectItem>
<SelectItem value="xa">Transplant</SelectItem>
<SelectItem value="xz">Cutting</SelectItem>
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="plantingDetails"
render={({ field }: { field: any }) => (
<FormItem>
<FormLabel className="font-bold text-lg">
Planting Details
</FormLabel>
<FormControl>
<div className="mt-5 space-y-5">
<div className="flex space-x-5">
<Textarea
id="plantingDetails"
className="w-96"
{...field}
/>
</div>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="pruningDetails"
render={({ field }: { field: any }) => (
<FormItem>
<FormLabel className="font-bold text-lg">
Pruning Details
</FormLabel>
<FormControl>
<div className="mt-5 space-y-5">
<div className="flex space-x-5">
<Textarea id="pruningDetails" className="w-96" {...field} />
</div>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="isPerennial"
render={({ field }: { field: any }) => (
<FormItem>
<FormControl>
<div className="mt-5 space-y-5">
<div className="flex space-x-5">
<Switch
checked={field.value}
onCheckedChange={field.onChange}
aria-readonly
/>
<p>Plant is Perennial</p>
</div>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="isPerennial"
render={({ field }: { field: any }) => (
<FormItem>
<FormControl>
<div className="mt-5 space-y-5">
<div className="flex space-x-5">
<Switch
checked={field.value}
onCheckedChange={field.onChange}
aria-readonly
/>
<p>Automatically create tasks for new plantings</p>
</div>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
);
}

View File

@ -0,0 +1,159 @@
"use client"
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { Check, ChevronDown, ChevronUp } from "lucide-react"
import { cn } from "@/lib/utils"
const Select = SelectPrimitive.Root
const SelectGroup = SelectPrimitive.Group
const SelectValue = SelectPrimitive.Value
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronUp className="h-4 w-4" />
</SelectPrimitive.ScrollUpButton>
))
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronDown className="h-4 w-4" />
</SelectPrimitive.ScrollDownButton>
))
SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
))
SelectContent.displayName = SelectPrimitive.Content.displayName
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn("px-2 py-1.5 text-sm font-semibold", className)}
{...props}
/>
))
SelectLabel.displayName = SelectPrimitive.Label.displayName
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
))
SelectItem.displayName = SelectPrimitive.Item.displayName
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
}

View File

@ -0,0 +1,29 @@
"use client"
import * as React from "react"
import * as SwitchPrimitives from "@radix-ui/react-switch"
import { cn } from "@/lib/utils"
const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
className
)}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb
className={cn(
"pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0"
)}
/>
</SwitchPrimitives.Root>
))
Switch.displayName = SwitchPrimitives.Root.displayName
export { Switch }

View File

@ -0,0 +1,22 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Textarea = React.forwardRef<
HTMLTextAreaElement,
React.ComponentProps<"textarea">
>(({ className, ...props }, ref) => {
return (
<textarea
className={cn(
"flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
ref={ref}
{...props}
/>
)
})
Textarea.displayName = "Textarea"
export { Textarea }

View File

@ -16,9 +16,12 @@
"@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-dialog": "^1.1.6",
"@radix-ui/react-dropdown-menu": "^2.1.6", "@radix-ui/react-dropdown-menu": "^2.1.6",
"@radix-ui/react-label": "^2.1.2", "@radix-ui/react-label": "^2.1.2",
"@radix-ui/react-select": "^2.1.6",
"@radix-ui/react-separator": "^1.1.2", "@radix-ui/react-separator": "^1.1.2",
"@radix-ui/react-slot": "^1.1.2", "@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-switch": "^1.1.3",
"@radix-ui/react-tooltip": "^1.1.8", "@radix-ui/react-tooltip": "^1.1.8",
"@react-google-maps/api": "^2.20.6",
"@tailwindcss/typography": "^0.5.16", "@tailwindcss/typography": "^0.5.16",
"@tanstack/react-query": "^5.66.0", "@tanstack/react-query": "^5.66.0",
"axios": "^1.7.9", "axios": "^1.7.9",

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,39 @@
import { z } from "zod";
const plantingDetailsFormSchema = z.object({
daysToEmerge: z.number().int().min(0, "Days to emerge must be at least 0"),
plantSpacing: z.number().min(0, "Plant spacing must be positive"),
rowSpacing: z.number().min(0, "Row spacing must be positive"),
plantingDepth: z.number().min(0, "Planting depth must be positive"),
averageHeight: z.number().min(0, "Average height must be positive"),
startMethod: z.string().optional(),
lightProfile: z.string().optional(),
soilConditions: z.string().optional(),
plantingDetails: z.string().optional(),
pruningDetails: z.string().optional(),
isPerennial: z.boolean(),
autoCreateTasks: z.boolean(),
});
const harvestDetailsFormSchema = z.object({
daysToFlower: z.number().int().min(0, "Days to flower must be at least 0"),
daysToMaturity: z
.number()
.int()
.min(0, "Days to maturity must be at least 0"),
harvestWindow: z.number().int().min(0, "Harvest window must be at least 0"),
estimatedLossRate: z
.number()
.min(0, "Loss rate must be positive")
.max(100, "Loss rate cannot exceed 100"),
harvestUnits: z.string().min(1, "Harvest units are required"),
estimatedRevenue: z.number().min(0, "Estimated revenue must be positive"),
expectedYieldPer100ft: z
.number()
.min(0, "Expected yield per 100ft must be positive"),
expectedYieldPerAcre: z
.number()
.min(0, "Expected yield per acre must be positive"),
});
export { plantingDetailsFormSchema, harvestDetailsFormSchema };