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() {
|
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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
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-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
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