mirror of
https://github.com/ForFarmTeam/ForFarm.git
synced 2025-12-18 13:34:08 +01:00
Merge branch 'main' into feature-knowledge-hub
This commit is contained in:
commit
aa87e3e26a
@ -22,11 +22,13 @@ type api struct {
|
||||
logger *slog.Logger
|
||||
httpClient *http.Client
|
||||
|
||||
userRepo domain.UserRepository
|
||||
cropRepo domain.CroplandRepository
|
||||
farmRepo domain.FarmRepository
|
||||
plantRepo domain.PlantRepository
|
||||
knowledgeHubRepo domain.KnowledgeHubRepository
|
||||
userRepo domain.UserRepository
|
||||
cropRepo domain.CroplandRepository
|
||||
farmRepo domain.FarmRepository
|
||||
plantRepo domain.PlantRepository
|
||||
inventoryRepo domain.InventoryRepository
|
||||
harvestRepo domain.HarvestRepository
|
||||
knowledgeHubRepo domain.KnowledgeHubRepository
|
||||
}
|
||||
|
||||
func NewAPI(ctx context.Context, logger *slog.Logger, pool *pgxpool.Pool) *api {
|
||||
@ -38,16 +40,20 @@ func NewAPI(ctx context.Context, logger *slog.Logger, pool *pgxpool.Pool) *api {
|
||||
farmRepository := repository.NewPostgresFarm(pool)
|
||||
plantRepository := repository.NewPostgresPlant(pool)
|
||||
knowledgeHubRepository := repository.NewPostgresKnowledgeHub(pool)
|
||||
inventoryRepository := repository.NewPostgresInventory(pool)
|
||||
harvestRepository := repository.NewPostgresHarvest(pool)
|
||||
|
||||
return &api{
|
||||
logger: logger,
|
||||
httpClient: client,
|
||||
|
||||
userRepo: userRepository,
|
||||
cropRepo: croplandRepository,
|
||||
farmRepo: farmRepository,
|
||||
plantRepo: plantRepository,
|
||||
knowledgeHubRepo: knowledgeHubRepository,
|
||||
userRepo: userRepository,
|
||||
cropRepo: croplandRepository,
|
||||
farmRepo: farmRepository,
|
||||
plantRepo: plantRepository,
|
||||
inventoryRepo: inventoryRepository,
|
||||
harvestRepo: harvestRepository,
|
||||
knowledgeHubRepo: knowledgeHubRepository,
|
||||
}
|
||||
}
|
||||
|
||||
@ -88,6 +94,7 @@ func (a *api) Routes() *chi.Mux {
|
||||
a.registerHelloRoutes(r, api)
|
||||
a.registerFarmRoutes(r, api)
|
||||
a.registerUserRoutes(r, api)
|
||||
a.registerInventoryRoutes(r, api)
|
||||
})
|
||||
|
||||
return router
|
||||
|
||||
409
backend/internal/api/inventory.go
Normal file
409
backend/internal/api/inventory.go
Normal file
@ -0,0 +1,409 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/danielgtaylor/huma/v2"
|
||||
"github.com/forfarm/backend/internal/domain"
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
func (a *api) registerInventoryRoutes(_ chi.Router, api huma.API) {
|
||||
tags := []string{"inventory"}
|
||||
prefix := "/inventory"
|
||||
|
||||
huma.Register(api, huma.Operation{
|
||||
OperationID: "createInventoryItem",
|
||||
Method: http.MethodPost,
|
||||
Path: prefix,
|
||||
Tags: tags,
|
||||
}, a.createInventoryItemHandler)
|
||||
|
||||
huma.Register(api, huma.Operation{
|
||||
OperationID: "getInventoryItemsByUser",
|
||||
Method: http.MethodGet,
|
||||
Path: prefix,
|
||||
Tags: tags,
|
||||
}, a.getInventoryItemsByUserHandler)
|
||||
|
||||
huma.Register(api, huma.Operation{
|
||||
OperationID: "getInventoryItem",
|
||||
Method: http.MethodGet,
|
||||
Path: prefix + "/{id}",
|
||||
Tags: tags,
|
||||
}, a.getInventoryItemHandler)
|
||||
|
||||
huma.Register(api, huma.Operation{
|
||||
OperationID: "updateInventoryItem",
|
||||
Method: http.MethodPut,
|
||||
Path: prefix + "/{id}",
|
||||
Tags: tags,
|
||||
}, a.updateInventoryItemHandler)
|
||||
|
||||
huma.Register(api, huma.Operation{
|
||||
OperationID: "deleteInventoryItem",
|
||||
Method: http.MethodDelete,
|
||||
Path: prefix + "/{id}",
|
||||
Tags: tags,
|
||||
}, a.deleteInventoryItemHandler)
|
||||
|
||||
huma.Register(api, huma.Operation{
|
||||
OperationID: "getInventoryStatus",
|
||||
Method: http.MethodGet,
|
||||
Path: prefix + "/status",
|
||||
Tags: tags,
|
||||
}, a.getInventoryStatusHandler)
|
||||
|
||||
huma.Register(api, huma.Operation{
|
||||
OperationID: "getInventoryCategory",
|
||||
Method: http.MethodGet,
|
||||
Path: prefix + "/category",
|
||||
Tags: tags,
|
||||
}, a.getInventoryCategoryHandler)
|
||||
|
||||
huma.Register(api, huma.Operation{
|
||||
OperationID: "getHarvestUnits",
|
||||
Method: http.MethodGet,
|
||||
Path: "/harvest/units",
|
||||
Tags: []string{"harvest"},
|
||||
}, a.getHarvestUnitsHandler)
|
||||
}
|
||||
|
||||
type InventoryItemResponse struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Category InventoryCategory `json:"category"`
|
||||
Quantity float64 `json:"quantity"`
|
||||
Unit HarvestUnit `json:"unit"`
|
||||
DateAdded time.Time `json:"date_added"`
|
||||
Status InventoryStatus `json:"status"`
|
||||
CreatedAt time.Time `json:"created_at,omitempty"`
|
||||
UpdatedAt time.Time `json:"updated_at,omitempty"`
|
||||
}
|
||||
|
||||
type InventoryStatus struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type InventoryCategory struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type HarvestUnit struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type CreateInventoryItemInput struct {
|
||||
Header string `header:"Authorization" required:"true" example:"Bearer token"`
|
||||
UserID string `header:"user_id" required:"true" example:"user-uuid"`
|
||||
Body struct {
|
||||
Name string `json:"name" required:"true"`
|
||||
CategoryID int `json:"category_id" required:"true"`
|
||||
Quantity float64 `json:"quantity" required:"true"`
|
||||
UnitID int `json:"unit_id" required:"true"`
|
||||
DateAdded time.Time `json:"date_added" required:"true"`
|
||||
StatusID int `json:"status_id" required:"true"`
|
||||
}
|
||||
}
|
||||
|
||||
type CreateInventoryItemOutput struct {
|
||||
Body struct {
|
||||
ID string `json:"id"`
|
||||
}
|
||||
}
|
||||
|
||||
type UpdateInventoryItemInput struct {
|
||||
Header string `header:"Authorization" required:"true" example:"Bearer token"`
|
||||
UserID string `header:"user_id" required:"true" example:"user-uuid"`
|
||||
ID string `path:"id"`
|
||||
Body struct {
|
||||
Name string `json:"name"`
|
||||
CategoryID int `json:"category_id"`
|
||||
Quantity float64 `json:"quantity"`
|
||||
UnitID int `json:"unit_id"`
|
||||
DateAdded time.Time `json:"date_added"`
|
||||
StatusID int `json:"status_id"`
|
||||
}
|
||||
}
|
||||
|
||||
type UpdateInventoryItemOutput struct {
|
||||
Body InventoryItemResponse
|
||||
}
|
||||
|
||||
type GetInventoryItemsInput struct {
|
||||
Header string `header:"Authorization" required:"true" example:"Bearer token"`
|
||||
UserID string `header:"user_id" required:"true" example:"user-uuid"`
|
||||
CategoryID int `query:"category_id"`
|
||||
StatusID int `query:"status_id"`
|
||||
StartDate time.Time `query:"start_date" format:"date-time"`
|
||||
EndDate time.Time `query:"end_date" format:"date-time"`
|
||||
SearchQuery string `query:"search"`
|
||||
SortBy string `query:"sort_by" enum:"name,quantity,date_added,created_at"`
|
||||
SortOrder string `query:"sort_order" enum:"asc,desc" default:"desc"`
|
||||
}
|
||||
|
||||
type GetInventoryItemsOutput struct {
|
||||
Body []InventoryItemResponse
|
||||
}
|
||||
|
||||
type GetInventoryItemInput struct {
|
||||
Header string `header:"Authorization" required:"true" example:"Bearer token"`
|
||||
UserID string `header:"user_id" required:"true" example:"user-uuid"`
|
||||
ID string `path:"id"`
|
||||
}
|
||||
|
||||
type GetInventoryItemOutput struct {
|
||||
Body InventoryItemResponse
|
||||
}
|
||||
|
||||
type DeleteInventoryItemInput struct {
|
||||
Header string `header:"Authorization" required:"true" example:"Bearer token"`
|
||||
UserID string `header:"user_id" required:"true" example:"user-uuid"`
|
||||
ID string `path:"id"`
|
||||
}
|
||||
|
||||
type DeleteInventoryItemOutput struct {
|
||||
Body struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
}
|
||||
|
||||
type GetInventoryStatusOutput struct {
|
||||
Body []InventoryStatus
|
||||
}
|
||||
|
||||
type GetInventoryCategoryOutput struct {
|
||||
Body []InventoryCategory
|
||||
}
|
||||
|
||||
type GetHarvestUnitsOutput struct {
|
||||
Body []HarvestUnit
|
||||
}
|
||||
|
||||
func (a *api) createInventoryItemHandler(ctx context.Context, input *CreateInventoryItemInput) (*CreateInventoryItemOutput, error) {
|
||||
item := &domain.InventoryItem{
|
||||
UserID: input.UserID,
|
||||
Name: input.Body.Name,
|
||||
CategoryID: input.Body.CategoryID,
|
||||
Quantity: input.Body.Quantity,
|
||||
UnitID: input.Body.UnitID,
|
||||
DateAdded: input.Body.DateAdded,
|
||||
StatusID: input.Body.StatusID,
|
||||
}
|
||||
|
||||
if err := item.Validate(); err != nil {
|
||||
return nil, huma.Error422UnprocessableEntity(err.Error())
|
||||
}
|
||||
|
||||
err := a.inventoryRepo.CreateOrUpdate(ctx, item)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &CreateInventoryItemOutput{Body: struct {
|
||||
ID string `json:"id"`
|
||||
}{ID: item.ID}}, nil
|
||||
}
|
||||
|
||||
func (a *api) getInventoryItemsByUserHandler(ctx context.Context, input *GetInventoryItemsInput) (*GetInventoryItemsOutput, error) {
|
||||
filter := domain.InventoryFilter{
|
||||
UserID: input.UserID,
|
||||
CategoryID: input.CategoryID,
|
||||
StatusID: input.StatusID,
|
||||
StartDate: input.StartDate,
|
||||
EndDate: input.EndDate,
|
||||
SearchQuery: input.SearchQuery,
|
||||
}
|
||||
|
||||
sort := domain.InventorySort{
|
||||
Field: input.SortBy,
|
||||
Direction: input.SortOrder,
|
||||
}
|
||||
|
||||
items, err := a.inventoryRepo.GetByUserID(ctx, input.UserID, filter, sort)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response := make([]InventoryItemResponse, len(items))
|
||||
for i, item := range items {
|
||||
response[i] = InventoryItemResponse{
|
||||
ID: item.ID,
|
||||
Name: item.Name,
|
||||
Category: InventoryCategory{
|
||||
ID: item.Category.ID,
|
||||
Name: item.Category.Name,
|
||||
},
|
||||
Quantity: item.Quantity,
|
||||
Unit: HarvestUnit{
|
||||
ID: item.Unit.ID,
|
||||
Name: item.Unit.Name,
|
||||
},
|
||||
DateAdded: item.DateAdded,
|
||||
Status: InventoryStatus{
|
||||
ID: item.Status.ID,
|
||||
Name: item.Status.Name,
|
||||
},
|
||||
CreatedAt: item.CreatedAt,
|
||||
UpdatedAt: item.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
return &GetInventoryItemsOutput{Body: response}, nil
|
||||
}
|
||||
|
||||
func (a *api) getInventoryItemHandler(ctx context.Context, input *GetInventoryItemInput) (*GetInventoryItemOutput, error) {
|
||||
item, err := a.inventoryRepo.GetByID(ctx, input.ID, input.UserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &GetInventoryItemOutput{Body: InventoryItemResponse{
|
||||
ID: item.ID,
|
||||
Name: item.Name,
|
||||
Category: InventoryCategory{
|
||||
ID: item.Category.ID,
|
||||
Name: item.Category.Name,
|
||||
},
|
||||
Quantity: item.Quantity,
|
||||
Unit: HarvestUnit{
|
||||
ID: item.Unit.ID,
|
||||
Name: item.Unit.Name,
|
||||
},
|
||||
DateAdded: item.DateAdded,
|
||||
Status: InventoryStatus{
|
||||
ID: item.Status.ID,
|
||||
Name: item.Status.Name,
|
||||
},
|
||||
CreatedAt: item.CreatedAt,
|
||||
UpdatedAt: item.UpdatedAt,
|
||||
}}, nil
|
||||
}
|
||||
|
||||
func (a *api) updateInventoryItemHandler(ctx context.Context, input *UpdateInventoryItemInput) (*UpdateInventoryItemOutput, error) {
|
||||
item, err := a.inventoryRepo.GetByID(ctx, input.ID, input.UserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if input.Body.Name != "" {
|
||||
item.Name = input.Body.Name
|
||||
}
|
||||
if input.Body.CategoryID != 0 {
|
||||
item.CategoryID = input.Body.CategoryID
|
||||
}
|
||||
if input.Body.Quantity != 0 {
|
||||
item.Quantity = input.Body.Quantity
|
||||
}
|
||||
if input.Body.UnitID != 0 {
|
||||
item.UnitID = input.Body.UnitID
|
||||
}
|
||||
if !input.Body.DateAdded.IsZero() {
|
||||
item.DateAdded = input.Body.DateAdded
|
||||
}
|
||||
if input.Body.StatusID != 0 {
|
||||
item.StatusID = input.Body.StatusID
|
||||
}
|
||||
|
||||
if err := item.Validate(); err != nil {
|
||||
return nil, huma.Error422UnprocessableEntity(err.Error())
|
||||
}
|
||||
|
||||
err = a.inventoryRepo.CreateOrUpdate(ctx, &item)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
updatedItem, err := a.inventoryRepo.GetByID(ctx, input.ID, input.UserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &UpdateInventoryItemOutput{Body: InventoryItemResponse{
|
||||
ID: updatedItem.ID,
|
||||
Name: updatedItem.Name,
|
||||
Category: InventoryCategory{
|
||||
ID: updatedItem.Category.ID,
|
||||
Name: updatedItem.Category.Name,
|
||||
},
|
||||
Quantity: updatedItem.Quantity,
|
||||
Unit: HarvestUnit{
|
||||
ID: updatedItem.Unit.ID,
|
||||
Name: updatedItem.Unit.Name,
|
||||
},
|
||||
DateAdded: updatedItem.DateAdded,
|
||||
Status: InventoryStatus{
|
||||
ID: updatedItem.Status.ID,
|
||||
Name: updatedItem.Status.Name,
|
||||
},
|
||||
CreatedAt: updatedItem.CreatedAt,
|
||||
UpdatedAt: updatedItem.UpdatedAt,
|
||||
}}, nil
|
||||
}
|
||||
|
||||
func (a *api) deleteInventoryItemHandler(ctx context.Context, input *DeleteInventoryItemInput) (*DeleteInventoryItemOutput, error) {
|
||||
err := a.inventoryRepo.Delete(ctx, input.ID, input.UserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &DeleteInventoryItemOutput{Body: struct {
|
||||
Message string `json:"message"`
|
||||
}{Message: "Inventory item deleted successfully"}}, nil
|
||||
}
|
||||
|
||||
func (a *api) getInventoryStatusHandler(ctx context.Context, input *struct{}) (*GetInventoryStatusOutput, error) {
|
||||
statuses, err := a.inventoryRepo.GetStatuses(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response := make([]InventoryStatus, len(statuses))
|
||||
for i, status := range statuses {
|
||||
response[i] = InventoryStatus{
|
||||
ID: status.ID,
|
||||
Name: status.Name,
|
||||
}
|
||||
}
|
||||
|
||||
return &GetInventoryStatusOutput{Body: response}, nil
|
||||
}
|
||||
|
||||
func (a *api) getInventoryCategoryHandler(ctx context.Context, input *struct{}) (*GetInventoryCategoryOutput, error) {
|
||||
categories, err := a.inventoryRepo.GetCategories(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response := make([]InventoryCategory, len(categories))
|
||||
for i, category := range categories {
|
||||
response[i] = InventoryCategory{
|
||||
ID: category.ID,
|
||||
Name: category.Name,
|
||||
}
|
||||
}
|
||||
|
||||
return &GetInventoryCategoryOutput{Body: response}, nil
|
||||
}
|
||||
|
||||
func (a *api) getHarvestUnitsHandler(ctx context.Context, input *struct{}) (*GetHarvestUnitsOutput, error) {
|
||||
units, err := a.harvestRepo.GetUnits(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response := make([]HarvestUnit, len(units))
|
||||
for i, unit := range units {
|
||||
response[i] = HarvestUnit{
|
||||
ID: unit.ID,
|
||||
Name: unit.Name,
|
||||
}
|
||||
}
|
||||
|
||||
return &GetHarvestUnitsOutput{Body: response}, nil
|
||||
}
|
||||
79
backend/internal/domain/inventory.go
Normal file
79
backend/internal/domain/inventory.go
Normal file
@ -0,0 +1,79 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
validation "github.com/go-ozzo/ozzo-validation/v4"
|
||||
)
|
||||
|
||||
type InventoryStatus struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type InventoryCategory struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type HarvestUnit struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type InventoryItem struct {
|
||||
ID string
|
||||
UserID string
|
||||
Name string
|
||||
CategoryID int
|
||||
Category InventoryCategory
|
||||
Quantity float64
|
||||
UnitID int
|
||||
Unit HarvestUnit
|
||||
DateAdded time.Time
|
||||
StatusID int
|
||||
Status InventoryStatus
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type InventoryFilter struct {
|
||||
UserID string
|
||||
CategoryID int
|
||||
StatusID int
|
||||
StartDate time.Time
|
||||
EndDate time.Time
|
||||
SearchQuery string
|
||||
}
|
||||
|
||||
type InventorySort struct {
|
||||
Field string
|
||||
Direction string
|
||||
}
|
||||
|
||||
func (i *InventoryItem) Validate() error {
|
||||
return validation.ValidateStruct(i,
|
||||
validation.Field(&i.UserID, validation.Required),
|
||||
validation.Field(&i.Name, validation.Required),
|
||||
validation.Field(&i.CategoryID, validation.Required),
|
||||
validation.Field(&i.Quantity, validation.Required, validation.Min(0.0)),
|
||||
validation.Field(&i.UnitID, validation.Required),
|
||||
validation.Field(&i.StatusID, validation.Required),
|
||||
validation.Field(&i.DateAdded, validation.Required),
|
||||
)
|
||||
}
|
||||
|
||||
type InventoryRepository interface {
|
||||
GetByID(ctx context.Context, id, userID string) (InventoryItem, error)
|
||||
GetByUserID(ctx context.Context, userID string, filter InventoryFilter, sort InventorySort) ([]InventoryItem, error)
|
||||
GetAll(ctx context.Context) ([]InventoryItem, error)
|
||||
CreateOrUpdate(ctx context.Context, item *InventoryItem) error
|
||||
Delete(ctx context.Context, id, userID string) error
|
||||
GetStatuses(ctx context.Context) ([]InventoryStatus, error)
|
||||
GetCategories(ctx context.Context) ([]InventoryCategory, error)
|
||||
}
|
||||
|
||||
type HarvestRepository interface {
|
||||
GetUnits(ctx context.Context) ([]HarvestUnit, error)
|
||||
}
|
||||
34
backend/internal/repository/postgres_harvest.go
Normal file
34
backend/internal/repository/postgres_harvest.go
Normal file
@ -0,0 +1,34 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/forfarm/backend/internal/domain"
|
||||
)
|
||||
|
||||
type postgresHarvestRepository struct {
|
||||
conn Connection
|
||||
}
|
||||
|
||||
func NewPostgresHarvest(conn Connection) domain.HarvestRepository {
|
||||
return &postgresHarvestRepository{conn: conn}
|
||||
}
|
||||
|
||||
func (p *postgresHarvestRepository) GetUnits(ctx context.Context) ([]domain.HarvestUnit, error) {
|
||||
query := `SELECT id, name FROM harvest_units ORDER BY id`
|
||||
rows, err := p.conn.Query(ctx, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var units []domain.HarvestUnit
|
||||
for rows.Next() {
|
||||
var u domain.HarvestUnit
|
||||
if err := rows.Scan(&u.ID, &u.Name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
units = append(units, u)
|
||||
}
|
||||
return units, nil
|
||||
}
|
||||
314
backend/internal/repository/postgres_inventory.go
Normal file
314
backend/internal/repository/postgres_inventory.go
Normal file
@ -0,0 +1,314 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/forfarm/backend/internal/domain"
|
||||
)
|
||||
|
||||
type postgresInventoryRepository struct {
|
||||
conn Connection
|
||||
}
|
||||
|
||||
func NewPostgresInventory(conn Connection) domain.InventoryRepository {
|
||||
return &postgresInventoryRepository{conn: conn}
|
||||
}
|
||||
|
||||
func (p *postgresInventoryRepository) fetch(ctx context.Context, query string, args ...interface{}) ([]domain.InventoryItem, error) {
|
||||
rows, err := p.conn.Query(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var items []domain.InventoryItem
|
||||
for rows.Next() {
|
||||
var i domain.InventoryItem
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.UserID,
|
||||
&i.Name,
|
||||
&i.CategoryID,
|
||||
&i.Quantity,
|
||||
&i.UnitID,
|
||||
&i.DateAdded,
|
||||
&i.StatusID,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (p *postgresInventoryRepository) GetByID(ctx context.Context, id, userID string) (domain.InventoryItem, error) {
|
||||
query := `
|
||||
SELECT
|
||||
i.id, i.user_id, i.name, i.category_id, i.quantity, i.unit_id,
|
||||
i.date_added, i.status_id, i.created_at, i.updated_at,
|
||||
c.name as category_name,
|
||||
s.name as status_name,
|
||||
u.name as unit_name
|
||||
FROM inventory_items i
|
||||
LEFT JOIN inventory_category c ON i.category_id = c.id
|
||||
LEFT JOIN inventory_status s ON i.status_id = s.id
|
||||
LEFT JOIN harvest_units u ON i.unit_id = u.id
|
||||
WHERE i.id = $1 AND i.user_id = $2`
|
||||
|
||||
rows, err := p.conn.Query(ctx, query, id, userID)
|
||||
if err != nil {
|
||||
return domain.InventoryItem{}, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
if !rows.Next() {
|
||||
return domain.InventoryItem{}, domain.ErrNotFound
|
||||
}
|
||||
|
||||
var item domain.InventoryItem
|
||||
err = rows.Scan(
|
||||
&item.ID,
|
||||
&item.UserID,
|
||||
&item.Name,
|
||||
&item.CategoryID,
|
||||
&item.Quantity,
|
||||
&item.UnitID,
|
||||
&item.DateAdded,
|
||||
&item.StatusID,
|
||||
&item.CreatedAt,
|
||||
&item.UpdatedAt,
|
||||
&item.Category.Name,
|
||||
&item.Status.Name,
|
||||
&item.Unit.Name,
|
||||
)
|
||||
if err != nil {
|
||||
return domain.InventoryItem{}, err
|
||||
}
|
||||
|
||||
return item, nil
|
||||
}
|
||||
|
||||
func (p *postgresInventoryRepository) GetByUserID(
|
||||
ctx context.Context,
|
||||
userID string,
|
||||
filter domain.InventoryFilter,
|
||||
sort domain.InventorySort,
|
||||
) ([]domain.InventoryItem, error) {
|
||||
var query strings.Builder
|
||||
args := []interface{}{userID}
|
||||
argPos := 2
|
||||
|
||||
query.WriteString(`
|
||||
SELECT
|
||||
i.id, i.user_id, i.name, i.category_id, i.quantity, i.unit_id,
|
||||
i.date_added, i.status_id, i.created_at, i.updated_at,
|
||||
c.name as category_name,
|
||||
s.name as status_name,
|
||||
u.name as unit_name
|
||||
FROM inventory_items i
|
||||
LEFT JOIN inventory_category c ON i.category_id = c.id
|
||||
LEFT JOIN inventory_status s ON i.status_id = s.id
|
||||
LEFT JOIN harvest_units u ON i.unit_id = u.id
|
||||
WHERE i.user_id = $1`)
|
||||
|
||||
if filter.CategoryID != 0 {
|
||||
query.WriteString(fmt.Sprintf(" AND i.category_id = $%d", argPos))
|
||||
args = append(args, filter.CategoryID)
|
||||
argPos++
|
||||
}
|
||||
|
||||
if filter.StatusID != 0 {
|
||||
query.WriteString(fmt.Sprintf(" AND i.status_id = $%d", argPos))
|
||||
args = append(args, filter.StatusID)
|
||||
argPos++
|
||||
}
|
||||
|
||||
if !filter.StartDate.IsZero() {
|
||||
query.WriteString(fmt.Sprintf(" AND i.date_added >= $%d", argPos))
|
||||
args = append(args, filter.StartDate)
|
||||
argPos++
|
||||
}
|
||||
|
||||
if !filter.EndDate.IsZero() {
|
||||
query.WriteString(fmt.Sprintf(" AND i.date_added <= $%d", argPos))
|
||||
args = append(args, filter.EndDate)
|
||||
argPos++
|
||||
}
|
||||
|
||||
if filter.SearchQuery != "" {
|
||||
query.WriteString(fmt.Sprintf(" AND i.name ILIKE $%d", argPos))
|
||||
args = append(args, "%"+filter.SearchQuery+"%")
|
||||
argPos++
|
||||
}
|
||||
|
||||
if sort.Field == "" {
|
||||
sort.Field = "i.date_added"
|
||||
sort.Direction = "desc"
|
||||
}
|
||||
|
||||
validSortFields := map[string]bool{
|
||||
"name": true,
|
||||
"quantity": true,
|
||||
"date_added": true,
|
||||
"created_at": true,
|
||||
}
|
||||
|
||||
if validSortFields[sort.Field] {
|
||||
query.WriteString(fmt.Sprintf(" ORDER BY %s", sort.Field))
|
||||
if strings.ToLower(sort.Direction) == "desc" {
|
||||
query.WriteString(" DESC")
|
||||
} else {
|
||||
query.WriteString(" ASC")
|
||||
}
|
||||
}
|
||||
|
||||
rows, err := p.conn.Query(ctx, query.String(), args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var items []domain.InventoryItem
|
||||
for rows.Next() {
|
||||
var item domain.InventoryItem
|
||||
err := rows.Scan(
|
||||
&item.ID,
|
||||
&item.UserID,
|
||||
&item.Name,
|
||||
&item.CategoryID,
|
||||
&item.Quantity,
|
||||
&item.UnitID,
|
||||
&item.DateAdded,
|
||||
&item.StatusID,
|
||||
&item.CreatedAt,
|
||||
&item.UpdatedAt,
|
||||
&item.Category.Name,
|
||||
&item.Status.Name,
|
||||
&item.Unit.Name,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (p *postgresInventoryRepository) GetAll(ctx context.Context) ([]domain.InventoryItem, error) {
|
||||
query := `
|
||||
SELECT
|
||||
i.id, i.user_id, i.name, i.category_id, i.quantity, i.unit_id,
|
||||
i.date_added, i.status_id, i.created_at, i.updated_at,
|
||||
c.name as category_name,
|
||||
s.name as status_name,
|
||||
u.name as unit_name
|
||||
FROM inventory_items i
|
||||
LEFT JOIN inventory_category c ON i.category_id = c.id
|
||||
LEFT JOIN inventory_status s ON i.status_id = s.id
|
||||
LEFT JOIN harvest_units u ON i.unit_id = u.id
|
||||
ORDER BY i.created_at DESC`
|
||||
return p.fetch(ctx, query)
|
||||
}
|
||||
|
||||
func (p *postgresInventoryRepository) CreateOrUpdate(ctx context.Context, item *domain.InventoryItem) error {
|
||||
now := time.Now()
|
||||
item.UpdatedAt = now
|
||||
|
||||
if item.ID == "" {
|
||||
item.CreatedAt = now
|
||||
query := `
|
||||
INSERT INTO inventory_items
|
||||
(id, user_id, name, category_id, quantity, unit_id, date_added, status_id, created_at, updated_at)
|
||||
VALUES (gen_random_uuid(), $1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||||
RETURNING id`
|
||||
return p.conn.QueryRow(
|
||||
ctx,
|
||||
query,
|
||||
item.UserID,
|
||||
item.Name,
|
||||
item.CategoryID,
|
||||
item.Quantity,
|
||||
item.UnitID,
|
||||
item.DateAdded,
|
||||
item.StatusID,
|
||||
item.CreatedAt,
|
||||
item.UpdatedAt,
|
||||
).Scan(&item.ID)
|
||||
}
|
||||
|
||||
query := `
|
||||
UPDATE inventory_items
|
||||
SET name = $1,
|
||||
category_id = $2,
|
||||
quantity = $3,
|
||||
unit_id = $4,
|
||||
date_added = $5,
|
||||
status_id = $6,
|
||||
updated_at = $7
|
||||
WHERE id = $8 AND user_id = $9
|
||||
RETURNING id`
|
||||
|
||||
return p.conn.QueryRow(
|
||||
ctx,
|
||||
query,
|
||||
item.Name,
|
||||
item.CategoryID,
|
||||
item.Quantity,
|
||||
item.UnitID,
|
||||
item.DateAdded,
|
||||
item.StatusID,
|
||||
item.UpdatedAt,
|
||||
item.ID,
|
||||
item.UserID,
|
||||
).Scan(&item.ID)
|
||||
}
|
||||
|
||||
func (p *postgresInventoryRepository) Delete(ctx context.Context, id, userID string) error {
|
||||
query := `DELETE FROM inventory_items WHERE id = $1 AND user_id = $2`
|
||||
_, err := p.conn.Exec(ctx, query, id, userID)
|
||||
return err
|
||||
}
|
||||
|
||||
func (p *postgresInventoryRepository) GetStatuses(ctx context.Context) ([]domain.InventoryStatus, error) {
|
||||
query := `SELECT id, name FROM inventory_status ORDER BY id`
|
||||
rows, err := p.conn.Query(ctx, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var statuses []domain.InventoryStatus
|
||||
for rows.Next() {
|
||||
var s domain.InventoryStatus
|
||||
if err := rows.Scan(&s.ID, &s.Name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
statuses = append(statuses, s)
|
||||
}
|
||||
return statuses, nil
|
||||
}
|
||||
|
||||
func (p *postgresInventoryRepository) GetCategories(ctx context.Context) ([]domain.InventoryCategory, error) {
|
||||
query := `SELECT id, name FROM inventory_category ORDER BY id`
|
||||
rows, err := p.conn.Query(ctx, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var categories []domain.InventoryCategory
|
||||
for rows.Next() {
|
||||
var c domain.InventoryCategory
|
||||
if err := rows.Scan(&c.ID, &c.Name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
categories = append(categories, c)
|
||||
}
|
||||
return categories, nil
|
||||
}
|
||||
@ -12,7 +12,7 @@ CREATE TABLE soil_conditions (
|
||||
CREATE TABLE harvest_units (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name TEXT NOT NULL UNIQUE
|
||||
);
|
||||
);
|
||||
|
||||
CREATE TABLE plants (
|
||||
uuid UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
|
||||
20
backend/migrations/00004_create_inventory_items_table.sql
Normal file
20
backend/migrations/00004_create_inventory_items_table.sql
Normal file
@ -0,0 +1,20 @@
|
||||
-- +goose Up
|
||||
CREATE TABLE inventory_items (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
category TEXT NOT NULL,
|
||||
type TEXT NOT NULL,
|
||||
quantity DOUBLE PRECISION NOT NULL,
|
||||
unit TEXT NOT NULL,
|
||||
date_added TIMESTAMPTZ NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT fk_inventory_items_user FOREIGN KEY (user_id) REFERENCES users(uuid) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Create indexes
|
||||
CREATE INDEX idx_inventory_items_user_id ON inventory_items(user_id);
|
||||
CREATE INDEX idx_inventory_items_user_category ON inventory_items(user_id, category);
|
||||
CREATE INDEX idx_inventory_items_user_status ON inventory_items(user_id, status);
|
||||
5
backend/migrations/00005_create_inventory_status.sql
Normal file
5
backend/migrations/00005_create_inventory_status.sql
Normal file
@ -0,0 +1,5 @@
|
||||
-- +goose Up
|
||||
CREATE TABLE inventory_status (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name TEXT NOT NULL UNIQUE
|
||||
);
|
||||
15
backend/migrations/00006_modify_inventory_table.sql
Normal file
15
backend/migrations/00006_modify_inventory_table.sql
Normal file
@ -0,0 +1,15 @@
|
||||
-- +goose Up
|
||||
ALTER TABLE inventory_items
|
||||
ADD COLUMN status_id INT;
|
||||
|
||||
UPDATE inventory_items
|
||||
SET status_id = (SELECT id FROM inventory_status WHERE name = inventory_items.status);
|
||||
|
||||
ALTER TABLE inventory_items
|
||||
DROP COLUMN status;
|
||||
|
||||
ALTER TABLE inventory_items
|
||||
ADD CONSTRAINT fk_inventory_items_status FOREIGN KEY (status_id) REFERENCES inventory_status(id) ON DELETE CASCADE;
|
||||
|
||||
CREATE INDEX idx_inventory_items_status_id ON inventory_items(status_id);
|
||||
|
||||
7
backend/migrations/00007_create_inventory_status.sql
Normal file
7
backend/migrations/00007_create_inventory_status.sql
Normal file
@ -0,0 +1,7 @@
|
||||
-- +goose Up
|
||||
-- Insert default statuses into the inventory_status table
|
||||
INSERT INTO inventory_status (name)
|
||||
VALUES
|
||||
('In Stock'),
|
||||
('Low Stock'),
|
||||
('Out Of Stock');
|
||||
@ -0,0 +1,70 @@
|
||||
-- +goose Up
|
||||
-- Step 1: Create inventory_category table
|
||||
CREATE TABLE inventory_category (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name TEXT NOT NULL UNIQUE
|
||||
);
|
||||
|
||||
-- Step 2: Insert sample categories
|
||||
INSERT INTO inventory_category (name)
|
||||
VALUES
|
||||
('Seeds'),
|
||||
('Tools'),
|
||||
('Chemicals');
|
||||
|
||||
-- Step 3: Add category_id column to inventory_items
|
||||
ALTER TABLE inventory_items
|
||||
ADD COLUMN category_id INT;
|
||||
|
||||
-- Step 4: Link inventory_items to inventory_category
|
||||
ALTER TABLE inventory_items
|
||||
ADD CONSTRAINT fk_inventory_category FOREIGN KEY (category_id) REFERENCES inventory_category(id) ON DELETE SET NULL;
|
||||
|
||||
-- Step 5: Remove old columns (type, category, unit) from inventory_items
|
||||
ALTER TABLE inventory_items
|
||||
DROP COLUMN type,
|
||||
DROP COLUMN category,
|
||||
DROP COLUMN unit;
|
||||
|
||||
-- Step 6: Add unit_id column to inventory_items
|
||||
ALTER TABLE inventory_items
|
||||
ADD COLUMN unit_id INT;
|
||||
|
||||
-- Step 7: Link inventory_items to harvest_units
|
||||
ALTER TABLE inventory_items
|
||||
ADD CONSTRAINT fk_inventory_unit FOREIGN KEY (unit_id) REFERENCES harvest_units(id) ON DELETE SET NULL;
|
||||
|
||||
-- Step 8: Insert new unit values into harvest_units
|
||||
INSERT INTO harvest_units (name)
|
||||
VALUES
|
||||
('Tonne'),
|
||||
('KG');
|
||||
|
||||
-- +goose Down
|
||||
-- Reverse Step 8: Remove inserted unit values
|
||||
DELETE FROM harvest_units WHERE name IN ('Tonne', 'KG');
|
||||
|
||||
-- Reverse Step 7: Remove the foreign key constraint
|
||||
ALTER TABLE inventory_items
|
||||
DROP CONSTRAINT fk_inventory_unit;
|
||||
|
||||
-- Reverse Step 6: Remove unit_id column from inventory_items
|
||||
ALTER TABLE inventory_items
|
||||
DROP COLUMN unit_id;
|
||||
|
||||
-- Reverse Step 5: Add back type, category, and unit columns
|
||||
ALTER TABLE inventory_items
|
||||
ADD COLUMN type TEXT NOT NULL,
|
||||
ADD COLUMN category TEXT NOT NULL,
|
||||
ADD COLUMN unit TEXT NOT NULL;
|
||||
|
||||
-- Reverse Step 4: Remove foreign key constraint from inventory_items
|
||||
ALTER TABLE inventory_items
|
||||
DROP CONSTRAINT fk_inventory_category;
|
||||
|
||||
-- Reverse Step 3: Remove category_id column from inventory_items
|
||||
ALTER TABLE inventory_items
|
||||
DROP COLUMN category_id;
|
||||
|
||||
-- Reverse Step 2: Drop inventory_category table
|
||||
DROP TABLE inventory_category;
|
||||
@ -1,11 +1,29 @@
|
||||
import axiosInstance from "./config";
|
||||
import type { InventoryItem, CreateInventoryItemInput } from "@/types";
|
||||
import type {
|
||||
InventoryItem,
|
||||
CreateInventoryItemInput,
|
||||
InventoryItemStatus,
|
||||
} from "@/types";
|
||||
|
||||
/**
|
||||
* Simulates an API call to fetch inventory items.
|
||||
* Waits for a simulated delay and then attempts an axios GET request.
|
||||
* If the request fails, returns fallback dummy data.
|
||||
*
|
||||
*
|
||||
*/
|
||||
export async function fetchInventoryStatus(): Promise<InventoryItemStatus[]> {
|
||||
try {
|
||||
const response = await axiosInstance.get<InventoryItemStatus[]>(
|
||||
"/inventory/status"
|
||||
);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error("Error fetching inventory status:", error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchInventoryItems(): Promise<InventoryItem[]> {
|
||||
try {
|
||||
const response = await axiosInstance.get<InventoryItem[]>("/api/inventory");
|
||||
@ -51,7 +69,7 @@ export async function fetchInventoryItems(): Promise<InventoryItem[]> {
|
||||
quantity: 150,
|
||||
unit: "kg",
|
||||
lastUpdated: "2023-03-15",
|
||||
status: "In Stock",
|
||||
status: "Out Of Stock",
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
@ -79,7 +97,10 @@ export async function createInventoryItem(
|
||||
// Simulate network delay
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
try {
|
||||
const response = await axiosInstance.post<InventoryItem>("/api/inventory", item);
|
||||
const response = await axiosInstance.post<InventoryItem>(
|
||||
"/api/inventory",
|
||||
item
|
||||
);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
// Simulate successful creation if API endpoint is not available
|
||||
|
||||
@ -18,7 +18,13 @@ import {
|
||||
CloudRain,
|
||||
Wind,
|
||||
} from "lucide-react";
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
} from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
@ -26,7 +32,11 @@ import { Badge } from "@/components/ui/badge";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { ChatbotDialog } from "./chatbot-dialog";
|
||||
import { AnalyticsDialog } from "./analytics-dialog";
|
||||
import { HoverCard, HoverCardContent, HoverCardTrigger } from "@/components/ui/hover-card";
|
||||
import {
|
||||
HoverCard,
|
||||
HoverCardContent,
|
||||
HoverCardTrigger,
|
||||
} from "@/components/ui/hover-card";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import type { Crop, CropAnalytics } from "@/types";
|
||||
import GoogleMapWithDrawing from "@/components/google-map-with-drawing";
|
||||
@ -37,7 +47,11 @@ interface CropDetailPageParams {
|
||||
cropId: string;
|
||||
}
|
||||
|
||||
export default function CropDetailPage({ params }: { params: Promise<CropDetailPageParams> }) {
|
||||
export default function CropDetailPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<CropDetailPageParams>;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const [crop, setCrop] = useState<Crop | null>(null);
|
||||
const [analytics, setAnalytics] = useState<CropAnalytics | null>(null);
|
||||
@ -57,7 +71,9 @@ export default function CropDetailPage({ params }: { params: Promise<CropDetailP
|
||||
|
||||
if (!crop || !analytics) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-background text-foreground">Loading...</div>
|
||||
<div className="min-h-screen flex items-center justify-center bg-background text-foreground">
|
||||
Loading...
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -87,7 +103,8 @@ export default function CropDetailPage({ params }: { params: Promise<CropDetailP
|
||||
icon: ListCollapse,
|
||||
description: "View detailed information",
|
||||
onClick: () => console.log("Details clicked"),
|
||||
color: "bg-purple-50 dark:bg-purple-900 text-purple-600 dark:text-purple-300",
|
||||
color:
|
||||
"bg-purple-50 dark:bg-purple-900 text-purple-600 dark:text-purple-300",
|
||||
},
|
||||
{
|
||||
title: "Settings",
|
||||
@ -107,7 +124,8 @@ export default function CropDetailPage({ params }: { params: Promise<CropDetailP
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="gap-2 text-green-700 dark:text-green-300 hover:text-green-800 dark:hover:text-green-200 hover:bg-green-100/50 dark:hover:bg-green-800/50"
|
||||
onClick={() => router.back()}>
|
||||
onClick={() => router.back()}
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" /> Back to Farm
|
||||
</Button>
|
||||
<HoverCard>
|
||||
@ -126,7 +144,9 @@ export default function CropDetailPage({ params }: { params: Promise<CropDetailP
|
||||
</Avatar>
|
||||
<div className="space-y-1">
|
||||
<h4 className="text-sm font-semibold">Growth Timeline</h4>
|
||||
<p className="text-sm text-muted-foreground">Planted on {crop.plantedDate.toLocaleDateString()}</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Planted on {crop.plantedDate.toLocaleDateString()}
|
||||
</p>
|
||||
<div className="flex items-center pt-2">
|
||||
<Separator className="w-full" />
|
||||
<span className="mx-2 text-xs text-muted-foreground">
|
||||
@ -150,19 +170,28 @@ export default function CropDetailPage({ params }: { params: Promise<CropDetailP
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex flex-col items-end">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline" className={`${healthColors[analytics.plantHealth]} border`}>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={`${healthColors[analytics.plantHealth]} border`}
|
||||
>
|
||||
Health Score: {crop.healthScore}%
|
||||
</Badge>
|
||||
<Badge variant="outline" className="bg-blue-50 dark:bg-blue-900 text-blue-600 dark:text-blue-300">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="bg-blue-50 dark:bg-blue-900 text-blue-600 dark:text-blue-300"
|
||||
>
|
||||
Growing
|
||||
</Badge>
|
||||
</div>
|
||||
{crop.expectedHarvest ? (
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
Expected harvest: {crop.expectedHarvest.toLocaleDateString()}
|
||||
Expected harvest:{" "}
|
||||
{crop.expectedHarvest.toLocaleDateString()}
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground mt-1">Expected harvest date not available</p>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
Expected harvest date not available
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@ -180,13 +209,18 @@ export default function CropDetailPage({ params }: { params: Promise<CropDetailP
|
||||
key={action.title}
|
||||
variant="outline"
|
||||
className={`h-auto p-4 flex flex-col items-center gap-3 transition-all group ${action.color} hover:scale-105`}
|
||||
onClick={action.onClick}>
|
||||
<div className={`p-3 rounded-lg ${action.color} group-hover:scale-110 transition-transform`}>
|
||||
onClick={action.onClick}
|
||||
>
|
||||
<div
|
||||
className={`p-3 rounded-lg ${action.color} group-hover:scale-110 transition-transform`}
|
||||
>
|
||||
<action.icon className="h-5 w-5" />
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="font-medium mb-1">{action.title}</div>
|
||||
<p className="text-xs text-muted-foreground">{action.description}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{action.description}
|
||||
</p>
|
||||
</div>
|
||||
</Button>
|
||||
))}
|
||||
@ -196,7 +230,9 @@ export default function CropDetailPage({ params }: { params: Promise<CropDetailP
|
||||
<Card className="border-green-100 dark:border-green-700">
|
||||
<CardHeader>
|
||||
<CardTitle>Environmental Conditions</CardTitle>
|
||||
<CardDescription>Real-time monitoring of growing conditions</CardDescription>
|
||||
<CardDescription>
|
||||
Real-time monitoring of growing conditions
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid gap-6">
|
||||
@ -247,15 +283,22 @@ export default function CropDetailPage({ params }: { params: Promise<CropDetailP
|
||||
].map((metric) => (
|
||||
<Card
|
||||
key={metric.label}
|
||||
className="border-none shadow-none bg-gradient-to-br from-white to-gray-50/50 dark:from-slate-800 dark:to-slate-700/50">
|
||||
className="border-none shadow-none bg-gradient-to-br from-white to-gray-50/50 dark:from-slate-800 dark:to-slate-700/50"
|
||||
>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className={`p-2 rounded-lg ${metric.bg}`}>
|
||||
<metric.icon className={`h-4 w-4 ${metric.color}`} />
|
||||
<metric.icon
|
||||
className={`h-4 w-4 ${metric.color}`}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-muted-foreground">{metric.label}</p>
|
||||
<p className="text-2xl font-semibold tracking-tight">{metric.value}</p>
|
||||
<p className="text-sm font-medium text-muted-foreground">
|
||||
{metric.label}
|
||||
</p>
|
||||
<p className="text-2xl font-semibold tracking-tight">
|
||||
{metric.value}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
@ -269,9 +312,14 @@ export default function CropDetailPage({ params }: { params: Promise<CropDetailP
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="font-medium">Growth Progress</span>
|
||||
<span className="text-muted-foreground">{analytics.growthProgress}%</span>
|
||||
<span className="text-muted-foreground">
|
||||
{analytics.growthProgress}%
|
||||
</span>
|
||||
</div>
|
||||
<Progress value={analytics.growthProgress} className="h-2" />
|
||||
<Progress
|
||||
value={analytics.growthProgress}
|
||||
className="h-2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Next Action Card */}
|
||||
@ -282,10 +330,15 @@ export default function CropDetailPage({ params }: { params: Promise<CropDetailP
|
||||
<Timer className="h-4 w-4 text-green-600 dark:text-green-300" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium mb-1">Next Action Required</p>
|
||||
<p className="text-sm text-muted-foreground">{analytics.nextAction}</p>
|
||||
<p className="font-medium mb-1">
|
||||
Next Action Required
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{analytics.nextAction}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Due by {analytics.nextActionDue.toLocaleDateString()}
|
||||
Due by{" "}
|
||||
{analytics.nextActionDue.toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -337,9 +390,14 @@ export default function CropDetailPage({ params }: { params: Promise<CropDetailP
|
||||
<div key={nutrient.name} className="space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="font-medium">{nutrient.name}</span>
|
||||
<span className="text-muted-foreground">{nutrient.value}%</span>
|
||||
<span className="text-muted-foreground">
|
||||
{nutrient.value}%
|
||||
</span>
|
||||
</div>
|
||||
<Progress value={nutrient.value} className={`h-2 ${nutrient.color}`} />
|
||||
<Progress
|
||||
value={nutrient.value}
|
||||
className={`h-2 ${nutrient.color}`}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@ -372,10 +430,14 @@ export default function CropDetailPage({ params }: { params: Promise<CropDetailP
|
||||
][i]
|
||||
}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">2 hours ago</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
2 hours ago
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{i < 4 && <Separator className="my-4 dark:bg-slate-700" />}
|
||||
{i < 4 && (
|
||||
<Separator className="my-4 dark:bg-slate-700" />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</ScrollArea>
|
||||
@ -385,8 +447,17 @@ export default function CropDetailPage({ params }: { params: Promise<CropDetailP
|
||||
</div>
|
||||
|
||||
{/* Dialogs */}
|
||||
<ChatbotDialog open={isChatOpen} onOpenChange={setIsChatOpen} cropName={crop.name} />
|
||||
<AnalyticsDialog open={isAnalyticsOpen} onOpenChange={setIsAnalyticsOpen} crop={crop} analytics={analytics} />
|
||||
<ChatbotDialog
|
||||
open={isChatOpen}
|
||||
onOpenChange={setIsChatOpen}
|
||||
cropName={crop.name}
|
||||
/>
|
||||
<AnalyticsDialog
|
||||
open={isAnalyticsOpen}
|
||||
onOpenChange={setIsAnalyticsOpen}
|
||||
crop={crop}
|
||||
analytics={analytics}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -399,9 +470,15 @@ function Activity({ icon }: { icon: number }) {
|
||||
const icons = [
|
||||
<Droplets key="0" className="h-4 w-4 text-blue-500 dark:text-blue-300" />,
|
||||
<Leaf key="1" className="h-4 w-4 text-green-500 dark:text-green-300" />,
|
||||
<LineChart key="2" className="h-4 w-4 text-purple-500 dark:text-purple-300" />,
|
||||
<LineChart
|
||||
key="2"
|
||||
className="h-4 w-4 text-purple-500 dark:text-purple-300"
|
||||
/>,
|
||||
<Sprout key="3" className="h-4 w-4 text-yellow-500 dark:text-yellow-300" />,
|
||||
<ThermometerSun key="4" className="h-4 w-4 text-orange-500 dark:text-orange-300" />,
|
||||
<ThermometerSun
|
||||
key="4"
|
||||
className="h-4 w-4 text-orange-500 dark:text-orange-300"
|
||||
/>,
|
||||
];
|
||||
return icons[icon];
|
||||
}
|
||||
|
||||
63
frontend/app/(sidebar)/inventory/delete-inventory-item.tsx
Normal file
63
frontend/app/(sidebar)/inventory/delete-inventory-item.tsx
Normal file
@ -0,0 +1,63 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { CalendarIcon } from "lucide-react";
|
||||
import { format } from "date-fns";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Calendar } from "@/components/ui/calendar";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { cn } from "@/lib/utils";
|
||||
// import { deleteInventoryItem } from "@/api/inventory";
|
||||
// import type { DeleteInventoryItemInput } from "@/types";
|
||||
|
||||
export function DeleteInventoryItem() {
|
||||
const [date, setDate] = useState<Date | undefined>();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [itemName, setItemName] = useState("");
|
||||
const [itemType, setItemType] = useState("");
|
||||
const [itemCategory, setItemCategory] = useState("");
|
||||
const [itemQuantity, setItemQuantity] = useState(0);
|
||||
const [itemUnit, setItemUnit] = useState("");
|
||||
|
||||
// const queryClient = useQueryClient();
|
||||
|
||||
const handleDelete = () => {
|
||||
// handle delete item
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
type="submit"
|
||||
className="bg-red-500 hover:bg-red-800 text-white"
|
||||
onClick={handleDelete}
|
||||
>
|
||||
Delete Item
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
203
frontend/app/(sidebar)/inventory/edit-inventory-item.tsx
Normal file
203
frontend/app/(sidebar)/inventory/edit-inventory-item.tsx
Normal file
@ -0,0 +1,203 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { CalendarIcon } from "lucide-react";
|
||||
import { format } from "date-fns";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Calendar } from "@/components/ui/calendar";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { cn } from "@/lib/utils";
|
||||
// import { updateInventoryItem } from "@/api/inventory";
|
||||
// import type { UpdateInventoryItemInput } from "@/types";
|
||||
|
||||
export interface EditInventoryItemProps {
|
||||
id: string;
|
||||
name: string;
|
||||
category: string;
|
||||
status: string;
|
||||
type: string;
|
||||
unit: string;
|
||||
quantity: number;
|
||||
}
|
||||
|
||||
export function EditInventoryItem({
|
||||
id,
|
||||
name,
|
||||
category,
|
||||
status,
|
||||
type,
|
||||
unit,
|
||||
quantity,
|
||||
}: EditInventoryItemProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [itemName, setItemName] = useState(name);
|
||||
const [itemType, setItemType] = useState(type);
|
||||
const [itemCategory, setItemCategory] = useState(category);
|
||||
const [itemQuantity, setItemQuantity] = useState(quantity);
|
||||
const [itemUnit, setItemUnit] = useState(unit);
|
||||
const [itemStatus, setItemStatus] = useState(status);
|
||||
|
||||
// const queryClient = useQueryClient();
|
||||
|
||||
// const mutation = useMutation({
|
||||
// mutationFn: (item: UpdateInventoryItemInput) => UpdateInventoryItem(item),
|
||||
// onSuccess: () => {
|
||||
// // Invalidate queries to refresh inventory data.
|
||||
// queryClient.invalidateQueries({ queryKey: ["inventoryItems"] });
|
||||
// // Reset form fields and close dialog.
|
||||
// setItemName("");
|
||||
// setItemType("");
|
||||
// setItemCategory("");
|
||||
// setItemQuantity(0);
|
||||
// setItemUnit("");
|
||||
// setDate(undefined);
|
||||
// setOpen(false);
|
||||
// },
|
||||
// });
|
||||
|
||||
const handleEdit = () => {
|
||||
// // Basic validation (you can extend this as needed)
|
||||
// if (!itemName || !itemType || !itemCategory || !itemUnit) return;
|
||||
// mutation.mutate({
|
||||
// name: itemName,
|
||||
// type: itemType,
|
||||
// category: itemCategory,
|
||||
// quantity: itemQuantity,
|
||||
// unit: itemUnit,
|
||||
// });
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="bg-yellow-500 hover:bg-yellow-600">Edit</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Edit Inventory Item</DialogTitle>
|
||||
<DialogDescription>
|
||||
Edit a plantation or fertilizer item in your inventory.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="name" className="text-right">
|
||||
Name
|
||||
</Label>
|
||||
<Input
|
||||
id="name"
|
||||
className="col-span-3"
|
||||
value={itemName}
|
||||
onChange={(e) => setItemName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="type" className="text-right">
|
||||
Type
|
||||
</Label>
|
||||
<Select value={itemType.toLowerCase()} onValueChange={setItemType}>
|
||||
<SelectTrigger className="col-span-3">
|
||||
<SelectValue placeholder="Select type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectLabel>Type</SelectLabel>
|
||||
<SelectItem value="plantation">Plantation</SelectItem>
|
||||
<SelectItem value="fertilizer">Fertilizer</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="type" className="text-right">
|
||||
Status
|
||||
</Label>
|
||||
<Select
|
||||
value={itemStatus.toLowerCase()}
|
||||
onValueChange={setItemStatus}
|
||||
>
|
||||
<SelectTrigger className="col-span-3">
|
||||
<SelectValue placeholder="Select status" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectLabel>Status</SelectLabel>
|
||||
<SelectItem value="in stock">In Stock</SelectItem>
|
||||
<SelectItem value="low stock">Low Stock</SelectItem>
|
||||
<SelectItem value="out of stock">Out Of Stock</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="category" className="text-right">
|
||||
Category
|
||||
</Label>
|
||||
<Input
|
||||
id="category"
|
||||
className="col-span-3"
|
||||
placeholder="e.g., Seeds, Organic"
|
||||
value={itemCategory}
|
||||
onChange={(e) => setItemCategory(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="quantity" className="text-right">
|
||||
Quantity
|
||||
</Label>
|
||||
<Input
|
||||
id="quantity"
|
||||
type="number"
|
||||
className="col-span-3"
|
||||
value={itemQuantity}
|
||||
onChange={(e) => setItemQuantity(Number(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="unit" className="text-right">
|
||||
Unit
|
||||
</Label>
|
||||
<Input
|
||||
id="unit"
|
||||
className="col-span-3"
|
||||
placeholder="e.g., kg, packets"
|
||||
value={itemUnit}
|
||||
onChange={(e) => setItemUnit(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button type="submit" onClick={handleEdit}>
|
||||
Edit Item
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@ -1,149 +1,252 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useState, useMemo } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Calendar, ChevronDown, Plus, Search } from "lucide-react";
|
||||
import {
|
||||
useReactTable,
|
||||
getCoreRowModel,
|
||||
getSortedRowModel,
|
||||
getPaginationRowModel,
|
||||
flexRender,
|
||||
SortingState,
|
||||
PaginationState,
|
||||
} from "@tanstack/react-table";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||
import { Pagination, PaginationContent, PaginationItem, PaginationLink } from "@/components/ui/pagination";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { Calendar as CalendarComponent } from "@/components/ui/calendar";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import {
|
||||
Pagination,
|
||||
PaginationContent,
|
||||
PaginationItem,
|
||||
} from "@/components/ui/pagination";
|
||||
import { Search } from "lucide-react";
|
||||
import { FaSort, FaSortUp, FaSortDown } from "react-icons/fa";
|
||||
|
||||
import { fetchInventoryItems } from "@/api/inventory";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { fetchInventoryItems, fetchInventoryStatus } from "@/api/inventory";
|
||||
import { AddInventoryItem } from "./add-inventory-item";
|
||||
import {
|
||||
EditInventoryItem,
|
||||
EditInventoryItemProps,
|
||||
} from "./edit-inventory-item";
|
||||
import { DeleteInventoryItem } from "./delete-inventory-item";
|
||||
|
||||
export default function InventoryPage() {
|
||||
const [date, setDate] = useState<Date>();
|
||||
const [inventoryType, setInventoryType] = useState("all");
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
});
|
||||
|
||||
// Fetch inventory items using react-query.
|
||||
const {
|
||||
data: inventoryItems,
|
||||
isLoading,
|
||||
isError,
|
||||
data: inventoryItems = [],
|
||||
isLoading: isItemLoading,
|
||||
isError: isItemError,
|
||||
} = useQuery({
|
||||
queryKey: ["inventoryItems"],
|
||||
queryFn: fetchInventoryItems,
|
||||
staleTime: 60 * 1000,
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
return <div className="flex min-h-screen bg-background items-center justify-center">Loading...</div>;
|
||||
}
|
||||
const {
|
||||
data: inventoryStatus = [],
|
||||
isLoading: isLoadingStatus,
|
||||
isError: isErrorStatus,
|
||||
} = useQuery({
|
||||
queryKey: ["inventoryStatus"],
|
||||
queryFn: fetchInventoryStatus,
|
||||
staleTime: 60 * 1000,
|
||||
});
|
||||
// console.table(inventoryItems);
|
||||
console.table(inventoryStatus);
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const filteredItems = useMemo(() => {
|
||||
return inventoryItems
|
||||
.map((item) => ({
|
||||
...item,
|
||||
id: String(item.id), // Convert `id` to string here
|
||||
}))
|
||||
.filter((item) =>
|
||||
item.name.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
}, [inventoryItems, searchTerm]);
|
||||
|
||||
if (isError || !inventoryItems) {
|
||||
return (
|
||||
<div className="flex min-h-screen bg-background items-center justify-center">Error loading inventory data.</div>
|
||||
);
|
||||
}
|
||||
const columns = [
|
||||
{ accessorKey: "name", header: "Name" },
|
||||
{ accessorKey: "category", header: "Category" },
|
||||
{ accessorKey: "quantity", header: "Quantity" },
|
||||
{ accessorKey: "unit", header: "Unit" },
|
||||
{ accessorKey: "lastUpdated", header: "Last Updated" },
|
||||
{
|
||||
accessorKey: "status",
|
||||
header: "Status",
|
||||
cell: (info: { getValue: () => string }) => {
|
||||
const status = info.getValue();
|
||||
|
||||
// Filter items based on selected type.
|
||||
const filteredItems =
|
||||
inventoryType === "all"
|
||||
? inventoryItems
|
||||
: inventoryItems.filter((item) =>
|
||||
inventoryType === "plantation" ? item.type === "Plantation" : item.type === "Fertilizer"
|
||||
let statusClass = ""; // default status class
|
||||
|
||||
if (status === "Low Stock") {
|
||||
statusClass = "bg-yellow-300"; // yellow for low stock
|
||||
} else if (status === "Out Of Stock") {
|
||||
statusClass = "bg-red-500 text-white"; // red for out of stock
|
||||
}
|
||||
|
||||
return (
|
||||
<Badge className={`py-1 px-2 rounded-md ${statusClass}`}>
|
||||
{status}
|
||||
</Badge>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "edit",
|
||||
header: "Edit",
|
||||
cell: ({ row }: { row: { original: EditInventoryItemProps } }) => (
|
||||
<EditInventoryItem {...row.original} />
|
||||
),
|
||||
enableSorting: false,
|
||||
},
|
||||
{
|
||||
accessorKey: "delete",
|
||||
header: "Delete",
|
||||
cell: () => <DeleteInventoryItem />,
|
||||
enableSorting: false,
|
||||
},
|
||||
];
|
||||
|
||||
const table = useReactTable({
|
||||
data: filteredItems,
|
||||
columns,
|
||||
state: { sorting, pagination },
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
onSortingChange: setSorting,
|
||||
onPaginationChange: setPagination,
|
||||
});
|
||||
|
||||
if (isItemLoading || isLoadingStatus)
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center">
|
||||
Loading...
|
||||
</div>
|
||||
);
|
||||
if (isItemError || isErrorStatus)
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center">
|
||||
Error loading inventory data.
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen bg-background">
|
||||
<div className="flex-1 flex flex-col">
|
||||
<main className="flex-1 p-6">
|
||||
<h1 className="text-2xl font-bold tracking-tight mb-6">Inventory</h1>
|
||||
|
||||
{/* Filters and search */}
|
||||
<div className="flex flex-col sm:flex-row gap-4 mb-6">
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant={inventoryType === "all" ? "default" : "outline"}
|
||||
onClick={() => setInventoryType("all")}
|
||||
className="w-24">
|
||||
All
|
||||
</Button>
|
||||
<Select value={inventoryType} onValueChange={setInventoryType}>
|
||||
<SelectTrigger className="w-32">
|
||||
<SelectValue placeholder="Crop" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value="all">All</SelectItem>
|
||||
<SelectItem value="plantation">Plantation</SelectItem>
|
||||
<SelectItem value="fertilizer">Fertilizer</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-1 gap-4">
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline" className="flex-1 justify-between">
|
||||
<div className="flex items-center">
|
||||
<Calendar className="mr-2 h-4 w-4" />
|
||||
{date ? date.toLocaleDateString() : "Time filter"}
|
||||
</div>
|
||||
<ChevronDown className="h-4 w-4 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0">
|
||||
<CalendarComponent mode="single" selected={date} onSelect={setDate} initialFocus />
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input type="search" placeholder="Search Farms" className="pl-8" />
|
||||
</div>
|
||||
|
||||
<AddInventoryItem />
|
||||
</div>
|
||||
<Search className="mt-1" />
|
||||
<Input
|
||||
type="search"
|
||||
placeholder="Search by name..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
<AddInventoryItem />
|
||||
</div>
|
||||
|
||||
{/* Table */}
|
||||
<div className="border rounded-md">
|
||||
<h3 className="px-4 py-2 border-b font-medium">Table Fields</h3>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Name</TableHead>
|
||||
<TableHead>Category</TableHead>
|
||||
<TableHead>Type</TableHead>
|
||||
<TableHead className="text-right">Quantity</TableHead>
|
||||
<TableHead>Last Updated</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
</TableRow>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => (
|
||||
<TableHead
|
||||
key={header.id}
|
||||
onClick={header.column.getToggleSortingHandler()}
|
||||
className="cursor-pointer "
|
||||
>
|
||||
<div className="flex items-center">
|
||||
{flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
{header.column.getCanSort() &&
|
||||
!header.column.columnDef.enableSorting && (
|
||||
<span className="ml-2">
|
||||
{header.column.getIsSorted() === "desc" ? (
|
||||
<FaSortDown />
|
||||
) : header.column.getIsSorted() === "asc" ? (
|
||||
<FaSortUp />
|
||||
) : (
|
||||
<FaSort />
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredItems.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={6} className="text-center py-8 text-muted-foreground">
|
||||
No inventory items found
|
||||
</TableCell>
|
||||
{table.getRowModel().rows.map((row) => (
|
||||
<TableRow key={row.id} className="even:bg-gray-800">
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id}>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext()
|
||||
)}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
) : (
|
||||
filteredItems.map((item) => (
|
||||
<TableRow key={item.id}>
|
||||
<TableCell className="font-medium">{item.name}</TableCell>
|
||||
<TableCell>{item.category}</TableCell>
|
||||
<TableCell>{item.type}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{item.quantity} {item.unit}
|
||||
</TableCell>
|
||||
<TableCell>{item.lastUpdated}</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant={item.status === "Low Stock" ? "destructive" : "default"}>{item.status}</Badge>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<Pagination className="mt-5">
|
||||
<PaginationContent className="space-x-5">
|
||||
<PaginationItem>
|
||||
<Button
|
||||
className="flex w-24"
|
||||
onClick={() =>
|
||||
setPagination((prev) => ({
|
||||
...prev,
|
||||
pageIndex: Math.max(0, prev.pageIndex - 1),
|
||||
}))
|
||||
}
|
||||
disabled={pagination.pageIndex === 0}
|
||||
>
|
||||
Previous
|
||||
</Button>
|
||||
</PaginationItem>
|
||||
|
||||
<PaginationItem>
|
||||
<Button
|
||||
className="flex w-24"
|
||||
onClick={() =>
|
||||
setPagination((prev) => ({
|
||||
...prev,
|
||||
pageIndex: prev.pageIndex + 1,
|
||||
}))
|
||||
}
|
||||
disabled={
|
||||
pagination.pageIndex >=
|
||||
Math.ceil(filteredItems.length / pagination.pageSize) - 1
|
||||
}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
</PaginationItem>
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -20,17 +20,37 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
type harvestSchema = z.infer<typeof harvestDetailsFormSchema>;
|
||||
|
||||
export default function HarvestDetailsForm() {
|
||||
export default function HarvestDetailsForm({
|
||||
onChange,
|
||||
}: {
|
||||
onChange: (data: harvestSchema) => void;
|
||||
}) {
|
||||
const form = useForm<harvestSchema>({
|
||||
resolver: zodResolver(harvestDetailsFormSchema),
|
||||
defaultValues: {},
|
||||
defaultValues: {
|
||||
daysToFlower: 0,
|
||||
daysToMaturity: 0,
|
||||
harvestWindow: 0,
|
||||
estimatedLossRate: 0,
|
||||
harvestUnits: "",
|
||||
estimatedRevenue: 0,
|
||||
expectedYieldPer100ft: 0,
|
||||
expectedYieldPerAcre: 0,
|
||||
},
|
||||
});
|
||||
const onSubmit: (data: harvestSchema) => void = (data) => {
|
||||
onChange(data);
|
||||
};
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form className="grid grid-cols-3 gap-5">
|
||||
<form
|
||||
className="grid grid-cols-3 gap-5"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="daysToFlower"
|
||||
@ -47,6 +67,13 @@ export default function HarvestDetailsForm() {
|
||||
id="daysToFlower"
|
||||
className="w-96"
|
||||
{...field}
|
||||
onChange={(e) => {
|
||||
// convert to number
|
||||
const value = e.target.value
|
||||
? parseInt(e.target.value, 10)
|
||||
: "";
|
||||
field.onChange(value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -71,6 +98,13 @@ export default function HarvestDetailsForm() {
|
||||
id="daysToMaturity"
|
||||
className="w-96"
|
||||
{...field}
|
||||
onChange={(e) => {
|
||||
// convert to number
|
||||
const value = e.target.value
|
||||
? parseInt(e.target.value, 10)
|
||||
: "";
|
||||
field.onChange(value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -95,6 +129,13 @@ export default function HarvestDetailsForm() {
|
||||
id="harvestWindow"
|
||||
className="w-96"
|
||||
{...field}
|
||||
onChange={(e) => {
|
||||
// convert to number
|
||||
const value = e.target.value
|
||||
? parseInt(e.target.value, 10)
|
||||
: "";
|
||||
field.onChange(value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -119,6 +160,13 @@ export default function HarvestDetailsForm() {
|
||||
id="estimatedLossRate"
|
||||
className="w-96"
|
||||
{...field}
|
||||
onChange={(e) => {
|
||||
// convert to number
|
||||
const value = e.target.value
|
||||
? parseInt(e.target.value, 10)
|
||||
: "";
|
||||
field.onChange(value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -168,6 +216,13 @@ export default function HarvestDetailsForm() {
|
||||
id="estimatedRevenue"
|
||||
className="w-96"
|
||||
{...field}
|
||||
onChange={(e) => {
|
||||
// convert to number
|
||||
const value = e.target.value
|
||||
? parseInt(e.target.value, 10)
|
||||
: "";
|
||||
field.onChange(value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -192,6 +247,13 @@ export default function HarvestDetailsForm() {
|
||||
id="expectedYieldPer100ft"
|
||||
className="w-96"
|
||||
{...field}
|
||||
onChange={(e) => {
|
||||
// convert to number
|
||||
const value = e.target.value
|
||||
? parseInt(e.target.value, 10)
|
||||
: "";
|
||||
field.onChange(value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -216,6 +278,13 @@ export default function HarvestDetailsForm() {
|
||||
id="expectedYieldPerAcre"
|
||||
className="w-96"
|
||||
{...field}
|
||||
onChange={(e) => {
|
||||
// convert to number
|
||||
const value = e.target.value
|
||||
? parseInt(e.target.value, 10)
|
||||
: "";
|
||||
field.onChange(value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -224,6 +293,9 @@ export default function HarvestDetailsForm() {
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div className="col-span-3 flex justify-center">
|
||||
<Button type="submit">Save</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
|
||||
@ -1,34 +1,124 @@
|
||||
"use client";
|
||||
import { SetStateAction, useEffect, useState } from "react";
|
||||
import PlantingDetailsForm from "./planting-detail-form";
|
||||
import HarvestDetailsForm from "./harvest-detail-form";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import GoogleMapWithDrawing from "@/components/google-map-with-drawing";
|
||||
import {
|
||||
plantingDetailsFormSchema,
|
||||
harvestDetailsFormSchema,
|
||||
} from "@/schemas/application.schema";
|
||||
import { z } from "zod";
|
||||
|
||||
type plantingSchema = z.infer<typeof plantingDetailsFormSchema>;
|
||||
type harvestSchema = z.infer<typeof harvestDetailsFormSchema>;
|
||||
|
||||
export default function SetupPage() {
|
||||
const [plantingDetails, setPlantingDetails] = useState<plantingSchema | null>(
|
||||
null
|
||||
);
|
||||
const [harvestDetails, setHarvestDetails] = useState<harvestSchema | null>(
|
||||
null
|
||||
);
|
||||
const [mapData, setMapData] = useState<{ lat: number; lng: number }[] | null>(
|
||||
null
|
||||
);
|
||||
|
||||
// handle planting details submission
|
||||
const handlePlantingDetailsChange = (data: plantingSchema) => {
|
||||
setPlantingDetails(data);
|
||||
};
|
||||
|
||||
// handle harvest details submission
|
||||
const handleHarvestDetailsChange = (data: harvestSchema) => {
|
||||
setHarvestDetails(data);
|
||||
};
|
||||
|
||||
// handle map area selection
|
||||
const handleMapDataChange = (data: { lat: number; lng: number }[]) => {
|
||||
setMapData((prevMapData) => {
|
||||
if (prevMapData) {
|
||||
return [...prevMapData, ...data];
|
||||
} else {
|
||||
return data;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// log the changes
|
||||
useEffect(() => {
|
||||
// console.log(plantingDetails);
|
||||
// console.log(harvestDetails);
|
||||
console.table(mapData);
|
||||
}, [plantingDetails, harvestDetails, mapData]);
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!plantingDetails || !harvestDetails || !mapData) {
|
||||
alert("Please complete all sections before submitting.");
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = {
|
||||
plantingDetails,
|
||||
harvestDetails,
|
||||
mapData,
|
||||
};
|
||||
|
||||
console.log("Form data to be submitted:", formData);
|
||||
|
||||
fetch("/api/submit", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(formData),
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
console.log("Response from backend:", data);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error submitting form:", error);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-5">
|
||||
<div className=" flex justify-center">
|
||||
<h1 className="flex text-2xl ">Plating Details</h1>
|
||||
{/* Planting Details Section */}
|
||||
<div className="flex justify-center">
|
||||
<h1 className="text-2xl">Planting Details</h1>
|
||||
</div>
|
||||
<Separator className="mt-3" />
|
||||
<div className="mt-10 flex justify-center">
|
||||
<PlantingDetailsForm />
|
||||
<PlantingDetailsForm onChange={handlePlantingDetailsChange} />
|
||||
</div>
|
||||
<div className=" flex justify-center mt-20">
|
||||
<h1 className="flex text-2xl ">Harvest Details</h1>
|
||||
|
||||
{/* Harvest Details Section */}
|
||||
<div className="flex justify-center mt-20">
|
||||
<h1 className="text-2xl">Harvest Details</h1>
|
||||
</div>
|
||||
<Separator className="mt-3" />
|
||||
<div className="mt-10 flex justify-center">
|
||||
<HarvestDetailsForm />
|
||||
<HarvestDetailsForm onChange={handleHarvestDetailsChange} />
|
||||
</div>
|
||||
|
||||
{/* Map Section */}
|
||||
<div className="mt-10">
|
||||
<div className=" flex justify-center mt-20">
|
||||
<h1 className="flex text-2xl ">Map</h1>
|
||||
<div className="flex justify-center mt-20">
|
||||
<h1 className="text-2xl">Map</h1>
|
||||
</div>
|
||||
<Separator className="mt-3" />
|
||||
<div className="mt-10">
|
||||
<GoogleMapWithDrawing />
|
||||
<GoogleMapWithDrawing onAreaSelected={handleMapDataChange} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Submit Button */}
|
||||
<div className="mt-10 flex justify-center">
|
||||
<button onClick={handleSubmit} className="btn btn-primary">
|
||||
Submit All Data
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -22,17 +22,42 @@ import {
|
||||
} from "@/components/ui/select";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
type plantingSchema = z.infer<typeof plantingDetailsFormSchema>;
|
||||
|
||||
export default function PlantingDetailsForm() {
|
||||
const form = useForm<plantingSchema>({
|
||||
export default function PlantingDetailsForm({
|
||||
onChange,
|
||||
}: {
|
||||
onChange: (data: plantingSchema) => void;
|
||||
}) {
|
||||
const form = useForm({
|
||||
resolver: zodResolver(plantingDetailsFormSchema),
|
||||
defaultValues: {},
|
||||
defaultValues: {
|
||||
daysToEmerge: 0,
|
||||
plantSpacing: 0,
|
||||
rowSpacing: 0,
|
||||
plantingDepth: 0,
|
||||
averageHeight: 0,
|
||||
startMethod: "",
|
||||
lightProfile: "",
|
||||
soilConditions: "",
|
||||
plantingDetails: "",
|
||||
pruningDetails: "",
|
||||
isPerennial: false,
|
||||
autoCreateTasks: false,
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = (data: plantingSchema) => {
|
||||
onChange(data);
|
||||
};
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form className="grid grid-cols-3 gap-5">
|
||||
<form
|
||||
className="grid grid-cols-3 gap-5"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="daysToEmerge"
|
||||
@ -47,6 +72,13 @@ export default function PlantingDetailsForm() {
|
||||
id="daysToEmerge"
|
||||
className="w-96"
|
||||
{...field}
|
||||
onChange={(e) => {
|
||||
// convert to number
|
||||
const value = e.target.value
|
||||
? parseInt(e.target.value, 10)
|
||||
: "";
|
||||
field.onChange(value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -69,6 +101,13 @@ export default function PlantingDetailsForm() {
|
||||
id="plantSpacing"
|
||||
className="w-96"
|
||||
{...field}
|
||||
onChange={(e) => {
|
||||
// convert to number
|
||||
const value = e.target.value
|
||||
? parseInt(e.target.value, 10)
|
||||
: "";
|
||||
field.onChange(value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -91,6 +130,13 @@ export default function PlantingDetailsForm() {
|
||||
id="rowSpacing"
|
||||
className="w-96"
|
||||
{...field}
|
||||
onChange={(e) => {
|
||||
// convert to number
|
||||
const value = e.target.value
|
||||
? parseInt(e.target.value, 10)
|
||||
: "";
|
||||
field.onChange(value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -115,6 +161,13 @@ export default function PlantingDetailsForm() {
|
||||
id="plantingDepth"
|
||||
className="w-96"
|
||||
{...field}
|
||||
onChange={(e) => {
|
||||
// convert to number
|
||||
const value = e.target.value
|
||||
? parseInt(e.target.value, 10)
|
||||
: "";
|
||||
field.onChange(value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -139,6 +192,13 @@ export default function PlantingDetailsForm() {
|
||||
id="averageHeight"
|
||||
className="w-96"
|
||||
{...field}
|
||||
onChange={(e) => {
|
||||
// convert to number
|
||||
const value = e.target.value
|
||||
? parseInt(e.target.value, 10)
|
||||
: "";
|
||||
field.onChange(value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -187,9 +247,9 @@ export default function PlantingDetailsForm() {
|
||||
<SelectValue placeholder="Select light profile" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="xp">Seed</SelectItem>
|
||||
<SelectItem value="xa">Transplant</SelectItem>
|
||||
<SelectItem value="xz">Cutting</SelectItem>
|
||||
<SelectItem value="Seed">Seed</SelectItem>
|
||||
<SelectItem value="Transplant">Transplant</SelectItem>
|
||||
<SelectItem value="Cutting">Cutting</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
@ -214,9 +274,9 @@ export default function PlantingDetailsForm() {
|
||||
<SelectValue placeholder="Select a soil condition" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="xp">Seed</SelectItem>
|
||||
<SelectItem value="xa">Transplant</SelectItem>
|
||||
<SelectItem value="xz">Cutting</SelectItem>
|
||||
<SelectItem value="Seed">Seed</SelectItem>
|
||||
<SelectItem value="Transplant">Transplant</SelectItem>
|
||||
<SelectItem value="Cutting">Cutting</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
@ -289,7 +349,7 @@ export default function PlantingDetailsForm() {
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="isPerennial"
|
||||
name="autoCreateTasks"
|
||||
render={({ field }: { field: any }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
@ -308,6 +368,9 @@ export default function PlantingDetailsForm() {
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div className="col-span-3 flex justify-center">
|
||||
<Button type="submit">Save</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import { GoogleMap, LoadScript, DrawingManager } from "@react-google-maps/api";
|
||||
import { useState, useCallback } from "react";
|
||||
|
||||
@ -10,17 +8,80 @@ const containerStyle = {
|
||||
|
||||
const center = { lat: 13.7563, lng: 100.5018 }; // Example: Bangkok, Thailand
|
||||
|
||||
const GoogleMapWithDrawing = () => {
|
||||
interface GoogleMapWithDrawingProps {
|
||||
onAreaSelected: (data: { lat: number; lng: number }[]) => void;
|
||||
}
|
||||
|
||||
const GoogleMapWithDrawing = ({
|
||||
onAreaSelected,
|
||||
}: GoogleMapWithDrawingProps) => {
|
||||
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);
|
||||
}, []);
|
||||
const onDrawingComplete = useCallback(
|
||||
(overlay: google.maps.drawing.OverlayCompleteEvent) => {
|
||||
const shape = overlay.overlay;
|
||||
|
||||
// check the shape of the drawing and extract lat/lng values
|
||||
if (shape instanceof google.maps.Polygon) {
|
||||
const path = shape.getPath();
|
||||
const coordinates = path.getArray().map((latLng) => ({
|
||||
lat: latLng.lat(),
|
||||
lng: latLng.lng(),
|
||||
}));
|
||||
console.log("Polygon coordinates:", coordinates);
|
||||
onAreaSelected(coordinates);
|
||||
} else if (shape instanceof google.maps.Rectangle) {
|
||||
const bounds = shape.getBounds();
|
||||
if (bounds) {
|
||||
const northEast = bounds.getNorthEast();
|
||||
const southWest = bounds.getSouthWest();
|
||||
const coordinates = [
|
||||
{ lat: northEast.lat(), lng: northEast.lng() },
|
||||
{ lat: southWest.lat(), lng: southWest.lng() },
|
||||
];
|
||||
console.log("Rectangle coordinates:", coordinates);
|
||||
onAreaSelected(coordinates);
|
||||
}
|
||||
} else if (shape instanceof google.maps.Circle) {
|
||||
const center = shape.getCenter();
|
||||
const radius = shape.getRadius();
|
||||
if (center) {
|
||||
const coordinates = [
|
||||
{
|
||||
lat: center.lat(),
|
||||
lng: center.lng(),
|
||||
radius: radius, // circle's radius in meters
|
||||
},
|
||||
];
|
||||
console.log("Circle center:", coordinates);
|
||||
onAreaSelected(coordinates);
|
||||
}
|
||||
} else if (shape instanceof google.maps.Polyline) {
|
||||
const path = shape.getPath();
|
||||
const coordinates = path.getArray().map((latLng) => ({
|
||||
lat: latLng.lat(),
|
||||
lng: latLng.lng(),
|
||||
}));
|
||||
console.log("Polyline coordinates:", coordinates);
|
||||
onAreaSelected(coordinates);
|
||||
} else {
|
||||
console.log("Unknown shape detected:", shape);
|
||||
}
|
||||
},
|
||||
[onAreaSelected]
|
||||
);
|
||||
|
||||
return (
|
||||
<LoadScript googleMapsApiKey={process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY!} libraries={["drawing"]}>
|
||||
<GoogleMap mapContainerStyle={containerStyle} center={center} zoom={10} onLoad={(map) => setMap(map)}>
|
||||
<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}
|
||||
|
||||
@ -31,6 +31,7 @@
|
||||
"@react-oauth/google": "^0.12.1",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@tanstack/react-query": "^5.66.0",
|
||||
"@tanstack/react-table": "^8.21.2",
|
||||
"axios": "^1.7.9",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
@ -45,6 +46,7 @@
|
||||
"react-day-picker": "8.10.1",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-hook-form": "^7.54.2",
|
||||
"react-icons": "^5.5.0",
|
||||
"recharts": "^2.15.1",
|
||||
"tailwind-merge": "^3.0.1",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -69,6 +69,10 @@ export type InventoryItem = {
|
||||
lastUpdated: string;
|
||||
status: string;
|
||||
};
|
||||
export type InventoryItemStatus = {
|
||||
id: number;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type CreateInventoryItemInput = Omit<InventoryItem, "id" | "lastUpdated" | "status">;
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user