diff --git a/backend/internal/api/api.go b/backend/internal/api/api.go index 2ee47ce..1875a5b 100644 --- a/backend/internal/api/api.go +++ b/backend/internal/api/api.go @@ -26,11 +26,12 @@ type api struct { httpClient *http.Client eventPublisher domain.EventPublisher - userRepo domain.UserRepository - cropRepo domain.CroplandRepository - farmRepo domain.FarmRepository - plantRepo domain.PlantRepository + userRepo domain.UserRepository + cropRepo domain.CroplandRepository + farmRepo domain.FarmRepository + plantRepo domain.PlantRepository inventoryRepo domain.InventoryRepository + harvestRepo domain.HarvestRepository } func NewAPI(ctx context.Context, logger *slog.Logger, pool *pgxpool.Pool, eventPublisher domain.EventPublisher) *api { @@ -42,6 +43,7 @@ func NewAPI(ctx context.Context, logger *slog.Logger, pool *pgxpool.Pool, eventP farmRepository := repository.NewPostgresFarm(pool) plantRepository := repository.NewPostgresPlant(pool) inventoryRepository := repository.NewPostgresInventory(pool) + harvestRepository := repository.NewPostgresHarvest(pool) farmRepository.SetEventPublisher(eventPublisher) @@ -50,11 +52,12 @@ func NewAPI(ctx context.Context, logger *slog.Logger, pool *pgxpool.Pool, eventP httpClient: client, eventPublisher: eventPublisher, - userRepo: userRepository, - cropRepo: croplandRepository, - farmRepo: farmRepository, - plantRepo: plantRepository, + userRepo: userRepository, + cropRepo: croplandRepository, + farmRepo: farmRepository, + plantRepo: plantRepository, inventoryRepo: inventoryRepository, + harvestRepo: harvestRepository, } } diff --git a/backend/internal/api/inventory.go b/backend/internal/api/inventory.go index 206f6b3..17e7eca 100644 --- a/backend/internal/api/inventory.go +++ b/backend/internal/api/inventory.go @@ -22,11 +22,11 @@ func (a *api) registerInventoryRoutes(_ chi.Router, api huma.API) { }, a.createInventoryItemHandler) huma.Register(api, huma.Operation{ - OperationID: "getInventoryItems", + OperationID: "getInventoryItemsByUser", Method: http.MethodGet, Path: prefix, Tags: tags, - }, a.getInventoryItemsHandler) + }, a.getInventoryItemsByUserHandler) huma.Register(api, huma.Operation{ OperationID: "getInventoryItem", @@ -48,18 +48,66 @@ func (a *api) registerInventoryRoutes(_ chi.Router, api huma.API) { 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"` - Category string `json:"category" required:"true"` - Type string `json:"type" required:"true"` - Quantity float64 `json:"quantity" required:"true"` - Unit string `json:"unit" required:"true"` - DateAdded time.Time `json:"date_added" required:"true"` - Status string `json:"status" required:"true" enum:"In Stock,Low Stock,Out of Stock"` + 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"` } } @@ -69,15 +117,83 @@ type CreateInventoryItemOutput struct { } } +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{ - Name: input.Body.Name, - Category: input.Body.Category, - Type: input.Body.Type, - Quantity: input.Body.Quantity, - Unit: input.Body.Unit, - DateAdded: input.Body.DateAdded, - Status: domain.InventoryStatus(input.Body.Status), + 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 { @@ -94,40 +210,11 @@ func (a *api) createInventoryItemHandler(ctx context.Context, input *CreateInven }{ID: item.ID}}, nil } -type GetInventoryItemsInput struct { - Header string `header:"Authorization" required:"true" example:"Bearer token"` - Category string `query:"category"` - Type string `query:"type"` - Status string `query:"status" enum:"In Stock,Low Stock,Out of Stock"` - 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,category,type,quantity,date_added,status,created_at"` - SortOrder string `query:"sort_order" enum:"asc,desc" default:"desc"` -} - -type InventoryItemResponse struct { - ID string `json:"id"` - Name string `json:"name"` - Category string `json:"category"` - Type string `json:"type"` - Quantity float64 `json:"quantity"` - Unit string `json:"unit"` - DateAdded time.Time `json:"date_added"` - Status string `json:"status"` - CreatedAt time.Time `json:"created_at,omitempty"` - UpdatedAt time.Time `json:"updated_at,omitempty"` -} - -type GetInventoryItemsOutput struct { - Body []InventoryItemResponse -} - -func (a *api) getInventoryItemsHandler(ctx context.Context, input *GetInventoryItemsInput) (*GetInventoryItemsOutput, error) { +func (a *api) getInventoryItemsByUserHandler(ctx context.Context, input *GetInventoryItemsInput) (*GetInventoryItemsOutput, error) { filter := domain.InventoryFilter{ - Category: input.Category, - Type: input.Type, - Status: domain.InventoryStatus(input.Status), + UserID: input.UserID, + CategoryID: input.CategoryID, + StatusID: input.StatusID, StartDate: input.StartDate, EndDate: input.EndDate, SearchQuery: input.SearchQuery, @@ -138,7 +225,7 @@ func (a *api) getInventoryItemsHandler(ctx context.Context, input *GetInventoryI Direction: input.SortOrder, } - items, err := a.inventoryRepo.GetWithFilter(ctx, filter, sort) + items, err := a.inventoryRepo.GetByUserID(ctx, input.UserID, filter, sort) if err != nil { return nil, err } @@ -146,14 +233,22 @@ func (a *api) getInventoryItemsHandler(ctx context.Context, input *GetInventoryI response := make([]InventoryItemResponse, len(items)) for i, item := range items { response[i] = InventoryItemResponse{ - ID: item.ID, - Name: item.Name, - Category: item.Category, - Type: item.Type, - Quantity: item.Quantity, - Unit: item.Unit, + 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: string(item.Status), + Status: InventoryStatus{ + ID: item.Status.ID, + Name: item.Status.Name, + }, CreatedAt: item.CreatedAt, UpdatedAt: item.UpdatedAt, } @@ -162,55 +257,36 @@ func (a *api) getInventoryItemsHandler(ctx context.Context, input *GetInventoryI return &GetInventoryItemsOutput{Body: response}, nil } -type GetInventoryItemInput struct { - Header string `header:"Authorization" required:"true" example:"Bearer token"` - ID string `path:"id"` -} - -type GetInventoryItemOutput struct { - Body InventoryItemResponse -} - func (a *api) getInventoryItemHandler(ctx context.Context, input *GetInventoryItemInput) (*GetInventoryItemOutput, error) { - item, err := a.inventoryRepo.GetByID(ctx, input.ID) + 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: item.Category, - Type: item.Type, - Quantity: item.Quantity, - Unit: item.Unit, + 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: string(item.Status), + Status: InventoryStatus{ + ID: item.Status.ID, + Name: item.Status.Name, + }, CreatedAt: item.CreatedAt, UpdatedAt: item.UpdatedAt, }}, nil } -type UpdateInventoryItemInput struct { - Header string `header:"Authorization" required:"true" example:"Bearer token"` - ID string `path:"id"` - Body struct { - Name string `json:"name"` - Category string `json:"category"` - Type string `json:"type"` - Quantity float64 `json:"quantity"` - Unit string `json:"unit"` - DateAdded time.Time `json:"date_added"` - Status string `json:"status" enum:"In Stock,Low Stock,Out of Stock"` - } -} - -type UpdateInventoryItemOutput struct { - Body InventoryItemResponse -} - func (a *api) updateInventoryItemHandler(ctx context.Context, input *UpdateInventoryItemInput) (*UpdateInventoryItemOutput, error) { - item, err := a.inventoryRepo.GetByID(ctx, input.ID) + item, err := a.inventoryRepo.GetByID(ctx, input.ID, input.UserID) if err != nil { return nil, err } @@ -218,23 +294,20 @@ func (a *api) updateInventoryItemHandler(ctx context.Context, input *UpdateInven if input.Body.Name != "" { item.Name = input.Body.Name } - if input.Body.Category != "" { - item.Category = input.Body.Category - } - if input.Body.Type != "" { - item.Type = input.Body.Type + if input.Body.CategoryID != 0 { + item.CategoryID = input.Body.CategoryID } if input.Body.Quantity != 0 { item.Quantity = input.Body.Quantity } - if input.Body.Unit != "" { - item.Unit = input.Body.Unit + if input.Body.UnitID != 0 { + item.UnitID = input.Body.UnitID } if !input.Body.DateAdded.IsZero() { item.DateAdded = input.Body.DateAdded } - if input.Body.Status != "" { - item.Status = domain.InventoryStatus(input.Body.Status) + if input.Body.StatusID != 0 { + item.StatusID = input.Body.StatusID } if err := item.Validate(); err != nil { @@ -246,38 +319,35 @@ func (a *api) updateInventoryItemHandler(ctx context.Context, input *UpdateInven return nil, err } - updatedItem, err := a.inventoryRepo.GetByID(ctx, input.ID) + 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: updatedItem.Category, - Type: updatedItem.Type, - Quantity: updatedItem.Quantity, - Unit: updatedItem.Unit, + 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: string(updatedItem.Status), + Status: InventoryStatus{ + ID: updatedItem.Status.ID, + Name: updatedItem.Status.Name, + }, CreatedAt: updatedItem.CreatedAt, UpdatedAt: updatedItem.UpdatedAt, }}, nil } -type DeleteInventoryItemInput struct { - Header string `header:"Authorization" required:"true" example:"Bearer token"` - ID string `path:"id"` -} - -type DeleteInventoryItemOutput struct { - Body struct { - Message string `json:"message"` - } -} - func (a *api) deleteInventoryItemHandler(ctx context.Context, input *DeleteInventoryItemInput) (*DeleteInventoryItemOutput, error) { - err := a.inventoryRepo.Delete(ctx, input.ID) + err := a.inventoryRepo.Delete(ctx, input.ID, input.UserID) if err != nil { return nil, err } @@ -286,3 +356,54 @@ func (a *api) deleteInventoryItemHandler(ctx context.Context, input *DeleteInven 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 +} diff --git a/backend/internal/domain/inventory.go b/backend/internal/domain/inventory.go index ecd6169..4b325d9 100644 --- a/backend/internal/domain/inventory.go +++ b/backend/internal/domain/inventory.go @@ -7,31 +7,41 @@ import ( validation "github.com/go-ozzo/ozzo-validation/v4" ) -type InventoryStatus string +type InventoryStatus struct { + ID int `json:"id"` + Name string `json:"name"` +} -const ( - StatusInStock InventoryStatus = "In Stock" - StatusLowStock InventoryStatus = "Low Stock" - StatusOutOfStock InventoryStatus = "Out of Stock" -) +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 - Name string - Category string - Type string - Quantity float64 - Unit string - DateAdded time.Time - Status InventoryStatus - CreatedAt time.Time - UpdatedAt time.Time + 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 { - Category string - Type string - Status InventoryStatus + UserID string + CategoryID int + StatusID int StartDate time.Time EndDate time.Time SearchQuery string @@ -44,18 +54,26 @@ type InventorySort struct { func (i *InventoryItem) Validate() error { return validation.ValidateStruct(i, + validation.Field(&i.UserID, validation.Required), validation.Field(&i.Name, validation.Required), - validation.Field(&i.Category, validation.Required), - validation.Field(&i.Type, validation.Required), + validation.Field(&i.CategoryID, validation.Required), validation.Field(&i.Quantity, validation.Required, validation.Min(0.0)), - validation.Field(&i.Unit, validation.Required), - validation.Field(&i.Status, validation.Required, validation.In(StatusInStock, StatusLowStock, StatusOutOfStock)), + 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 string) (InventoryItem, error) - GetWithFilter(ctx context.Context, filter InventoryFilter, sort InventorySort) ([]InventoryItem, error) + 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 string) 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) } diff --git a/backend/internal/repository/postgres_harvest.go b/backend/internal/repository/postgres_harvest.go new file mode 100644 index 0000000..3bfc40a --- /dev/null +++ b/backend/internal/repository/postgres_harvest.go @@ -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 +} diff --git a/backend/internal/repository/postgres_inventory.go b/backend/internal/repository/postgres_inventory.go index 1a05655..1bd5d98 100644 --- a/backend/internal/repository/postgres_inventory.go +++ b/backend/internal/repository/postgres_inventory.go @@ -29,13 +29,13 @@ func (p *postgresInventoryRepository) fetch(ctx context.Context, query string, a var i domain.InventoryItem if err := rows.Scan( &i.ID, + &i.UserID, &i.Name, - &i.Category, - &i.Type, + &i.CategoryID, &i.Quantity, - &i.Unit, + &i.UnitID, &i.DateAdded, - &i.Status, + &i.StatusID, &i.CreatedAt, &i.UpdatedAt, ); err != nil { @@ -46,80 +46,115 @@ func (p *postgresInventoryRepository) fetch(ctx context.Context, query string, a return items, nil } -func (p *postgresInventoryRepository) GetByID(ctx context.Context, id string) (domain.InventoryItem, error) { +func (p *postgresInventoryRepository) GetByID(ctx context.Context, id, userID string) (domain.InventoryItem, error) { query := ` - SELECT id, name, category, type, quantity, unit, date_added, status, created_at, updated_at - FROM inventory_items - WHERE id = $1` + 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` - items, err := p.fetch(ctx, query, id) + rows, err := p.conn.Query(ctx, query, id, userID) if err != nil { return domain.InventoryItem{}, err } - if len(items) == 0 { + defer rows.Close() + + if !rows.Next() { return domain.InventoryItem{}, domain.ErrNotFound } - return items[0], nil + + 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) GetWithFilter(ctx context.Context, filter domain.InventoryFilter, sort domain.InventorySort) ([]domain.InventoryItem, error) { +func (p *postgresInventoryRepository) GetByUserID( + ctx context.Context, + userID string, + filter domain.InventoryFilter, + sort domain.InventorySort, +) ([]domain.InventoryItem, error) { var query strings.Builder - args := []interface{}{} - argPos := 1 + args := []interface{}{userID} + argPos := 2 query.WriteString(` - SELECT id, name, category, type, quantity, unit, date_added, status, created_at, updated_at - FROM inventory_items - WHERE 1=1`) + 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.Category != "" { - query.WriteString(fmt.Sprintf(" AND category = $%d", argPos)) - args = append(args, filter.Category) + if filter.CategoryID != 0 { + query.WriteString(fmt.Sprintf(" AND i.category_id = $%d", argPos)) + args = append(args, filter.CategoryID) argPos++ } - if filter.Type != "" { - query.WriteString(fmt.Sprintf(" AND type = $%d", argPos)) - args = append(args, filter.Type) - argPos++ - } - - if filter.Status != "" { - query.WriteString(fmt.Sprintf(" AND status = $%d", argPos)) - args = append(args, filter.Status) + 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 date_added >= $%d", argPos)) + 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 date_added <= $%d", argPos)) + query.WriteString(fmt.Sprintf(" AND i.date_added <= $%d", argPos)) args = append(args, filter.EndDate) argPos++ } if filter.SearchQuery != "" { - query.WriteString(fmt.Sprintf(" AND name ILIKE $%d", argPos)) + query.WriteString(fmt.Sprintf(" AND i.name ILIKE $%d", argPos)) args = append(args, "%"+filter.SearchQuery+"%") argPos++ } if sort.Field == "" { - sort.Field = "date_added" + sort.Field = "i.date_added" sort.Direction = "desc" } validSortFields := map[string]bool{ "name": true, - "category": true, - "type": true, "quantity": true, "date_added": true, - "status": true, "created_at": true, } @@ -132,7 +167,53 @@ func (p *postgresInventoryRepository) GetWithFilter(ctx context.Context, filter } } - return p.fetch(ctx, query.String(), args...) + 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 { @@ -143,19 +224,19 @@ func (p *postgresInventoryRepository) CreateOrUpdate(ctx context.Context, item * item.CreatedAt = now query := ` INSERT INTO inventory_items - (id, name, category, type, quantity, unit, date_added, status, created_at, updated_at) + (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.Category, - item.Type, + item.CategoryID, item.Quantity, - item.Unit, + item.UnitID, item.DateAdded, - item.Status, + item.StatusID, item.CreatedAt, item.UpdatedAt, ).Scan(&item.ID) @@ -164,33 +245,70 @@ func (p *postgresInventoryRepository) CreateOrUpdate(ctx context.Context, item * query := ` UPDATE inventory_items SET name = $1, - category = $2, - type = $3, - quantity = $4, - unit = $5, - date_added = $6, - status = $7, - updated_at = $8 - WHERE id = $9 + 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.Category, - item.Type, + item.CategoryID, item.Quantity, - item.Unit, + item.UnitID, item.DateAdded, - item.Status, + item.StatusID, item.UpdatedAt, item.ID, + item.UserID, ).Scan(&item.ID) } -func (p *postgresInventoryRepository) Delete(ctx context.Context, id string) error { - query := `DELETE FROM inventory_items WHERE id = $1` - _, err := p.conn.Exec(ctx, query, 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 +} diff --git a/backend/migrations/00002_create_farm_and_cropland_tables.sql b/backend/migrations/00002_create_farm_and_cropland_tables.sql index a0e0447..4e4cc70 100644 --- a/backend/migrations/00002_create_farm_and_cropland_tables.sql +++ b/backend/migrations/00002_create_farm_and_cropland_tables.sql @@ -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(), diff --git a/backend/migrations/00004_create_inventory_items_table.sql b/backend/migrations/00004_create_inventory_items_table.sql index eaf6945..905726f 100644 --- a/backend/migrations/00004_create_inventory_items_table.sql +++ b/backend/migrations/00004_create_inventory_items_table.sql @@ -1,6 +1,7 @@ -- +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, @@ -9,8 +10,11 @@ CREATE TABLE inventory_items ( date_added TIMESTAMPTZ NOT NULL, status TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - updated_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 INDEX idx_inventory_items_category ON inventory_items(category); -CREATE INDEX idx_inventory_items_status ON inventory_items(status); +-- 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); diff --git a/backend/migrations/00005_create_inventory_status.sql b/backend/migrations/00005_create_inventory_status.sql new file mode 100644 index 0000000..4f634f2 --- /dev/null +++ b/backend/migrations/00005_create_inventory_status.sql @@ -0,0 +1,5 @@ +-- +goose Up +CREATE TABLE inventory_status ( + id SERIAL PRIMARY KEY, + name TEXT NOT NULL UNIQUE +); diff --git a/backend/migrations/00006_modify_inventory_table.sql b/backend/migrations/00006_modify_inventory_table.sql new file mode 100644 index 0000000..44fb45c --- /dev/null +++ b/backend/migrations/00006_modify_inventory_table.sql @@ -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); + diff --git a/backend/migrations/00007_create_inventory_status.sql b/backend/migrations/00007_create_inventory_status.sql new file mode 100644 index 0000000..7e54cc9 --- /dev/null +++ b/backend/migrations/00007_create_inventory_status.sql @@ -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'); \ No newline at end of file diff --git a/backend/migrations/00008_modify_inventory_and_harvest_units.sql b/backend/migrations/00008_modify_inventory_and_harvest_units.sql new file mode 100644 index 0000000..e3b32dd --- /dev/null +++ b/backend/migrations/00008_modify_inventory_and_harvest_units.sql @@ -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; diff --git a/frontend/api/inventory.ts b/frontend/api/inventory.ts index 69fd6b3..1c59e20 100644 --- a/frontend/api/inventory.ts +++ b/frontend/api/inventory.ts @@ -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 { + try { + const response = await axiosInstance.get( + "/inventory/status" + ); + return response.data; + } catch (error) { + console.error("Error fetching inventory status:", error); + return []; + } +} + export async function fetchInventoryItems(): Promise { try { const response = await axiosInstance.get("/api/inventory"); @@ -51,7 +69,7 @@ export async function fetchInventoryItems(): Promise { quantity: 150, unit: "kg", lastUpdated: "2023-03-15", - status: "In Stock", + status: "Out Of Stock", }, { id: 5, diff --git a/frontend/app/(sidebar)/inventory/edit-inventory-item.tsx b/frontend/app/(sidebar)/inventory/edit-inventory-item.tsx index 0affb70..b5d2a7a 100644 --- a/frontend/app/(sidebar)/inventory/edit-inventory-item.tsx +++ b/frontend/app/(sidebar)/inventory/edit-inventory-item.tsx @@ -36,14 +36,32 @@ import { cn } from "@/lib/utils"; // import { updateInventoryItem } from "@/api/inventory"; // import type { UpdateInventoryItemInput } from "@/types"; -export function EditInventoryItem() { - const [date, setDate] = useState(); +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(""); - const [itemType, setItemType] = useState(""); - const [itemCategory, setItemCategory] = useState(""); - const [itemQuantity, setItemQuantity] = useState(0); - const [itemUnit, setItemUnit] = useState(""); + 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(); @@ -103,7 +121,7 @@ export function EditInventoryItem() { - @@ -116,6 +134,27 @@ export function EditInventoryItem() { +
+ + +
-
- - - - - - - - - -