mirror of
https://github.com/ForFarmTeam/ForFarm.git
synced 2025-12-19 14:04:08 +01:00
Merge branch 'main' into feature-authen
This commit is contained in:
commit
e52c003470
36
backend/internal/domain/cropland.go
Normal file
36
backend/internal/domain/cropland.go
Normal 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
|
||||
}
|
||||
33
backend/internal/domain/farm.go
Normal file
33
backend/internal/domain/farm.go
Normal 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
|
||||
}
|
||||
111
backend/internal/repository/postgres_cropland.go
Normal file
111
backend/internal/repository/postgres_cropland.go
Normal 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
|
||||
}
|
||||
102
backend/internal/repository/postgres_farm.go
Normal file
102
backend/internal/repository/postgres_farm.go
Normal 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
|
||||
}
|
||||
67
backend/migrations/00002_create_farm_and_cropland_tables.sql
Normal file
67
backend/migrations/00002_create_farm_and_cropland_tables.sql
Normal 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
|
||||
);
|
||||
67
frontend/app/setup/google-map-with-drawing.tsx
Normal file
67
frontend/app/setup/google-map-with-drawing.tsx
Normal 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;
|
||||
230
frontend/app/setup/harvest-detail-form.tsx
Normal file
230
frontend/app/setup/harvest-detail-form.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -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() {
|
||||
return (
|
||||
<div>
|
||||
<h1>Setup Page</h1>
|
||||
<div className="p-5">
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
314
frontend/app/setup/planting-detail-form.tsx
Normal file
314
frontend/app/setup/planting-detail-form.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
159
frontend/components/ui/select.tsx
Normal file
159
frontend/components/ui/select.tsx
Normal 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,
|
||||
}
|
||||
29
frontend/components/ui/switch.tsx
Normal file
29
frontend/components/ui/switch.tsx
Normal 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 }
|
||||
22
frontend/components/ui/textarea.tsx
Normal file
22
frontend/components/ui/textarea.tsx
Normal 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 }
|
||||
@ -16,9 +16,12 @@
|
||||
"@radix-ui/react-dialog": "^1.1.6",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.6",
|
||||
"@radix-ui/react-label": "^2.1.2",
|
||||
"@radix-ui/react-select": "^2.1.6",
|
||||
"@radix-ui/react-separator": "^1.1.2",
|
||||
"@radix-ui/react-slot": "^1.1.2",
|
||||
"@radix-ui/react-switch": "^1.1.3",
|
||||
"@radix-ui/react-tooltip": "^1.1.8",
|
||||
"@react-google-maps/api": "^2.20.6",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@tanstack/react-query": "^5.66.0",
|
||||
"axios": "^1.7.9",
|
||||
|
||||
10656
frontend/pnpm-lock.yaml
10656
frontend/pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
39
frontend/schemas/application.schema.ts
Normal file
39
frontend/schemas/application.schema.ts
Normal 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 };
|
||||
Loading…
Reference in New Issue
Block a user