From b39ddfba6f7656eac876c1fe6bd413574c472e2b Mon Sep 17 00:00:00 2001 From: Natthapol SERMSARAN Date: Tue, 1 Apr 2025 03:51:06 +0700 Subject: [PATCH 01/16] feat: add user to inventory_items table in migration file --- .../migrations/00004_create_inventory_items_table.sql | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) 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); From 5a373b5f2bbe2f53907879a7a79576703b99aa94 Mon Sep 17 00:00:00 2001 From: Natthapol SERMSARAN Date: Tue, 1 Apr 2025 03:51:31 +0700 Subject: [PATCH 02/16] feat: add user to inventory repository --- .../internal/repository/postgres_inventory.go | 46 +++++++++++++------ 1 file changed, 31 insertions(+), 15 deletions(-) diff --git a/backend/internal/repository/postgres_inventory.go b/backend/internal/repository/postgres_inventory.go index 1a05655..5dd0319 100644 --- a/backend/internal/repository/postgres_inventory.go +++ b/backend/internal/repository/postgres_inventory.go @@ -29,6 +29,7 @@ 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, @@ -46,13 +47,13 @@ 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 + SELECT id, user_id, name, category, type, quantity, unit, date_added, status, created_at, updated_at FROM inventory_items - WHERE id = $1` + WHERE id = $1 AND user_id = $2` - items, err := p.fetch(ctx, query, id) + items, err := p.fetch(ctx, query, id, userID) if err != nil { return domain.InventoryItem{}, err } @@ -62,15 +63,20 @@ func (p *postgresInventoryRepository) GetByID(ctx context.Context, id string) (d return items[0], 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 + SELECT id, user_id, name, category, type, quantity, unit, date_added, status, created_at, updated_at FROM inventory_items - WHERE 1=1`) + WHERE user_id = $1`) if filter.Category != "" { query.WriteString(fmt.Sprintf(" AND category = $%d", argPos)) @@ -135,6 +141,14 @@ func (p *postgresInventoryRepository) GetWithFilter(ctx context.Context, filter return p.fetch(ctx, query.String(), args...) } +func (p *postgresInventoryRepository) GetAll(ctx context.Context) ([]domain.InventoryItem, error) { + query := ` + SELECT id, user_id, name, category, type, quantity, unit, date_added, status, created_at, updated_at + FROM inventory_items + ORDER BY 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 @@ -143,12 +157,13 @@ 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) - VALUES (gen_random_uuid(), $1, $2, $3, $4, $5, $6, $7, $8, $9) + (id, user_id, name, category, type, quantity, unit, date_added, status, created_at, updated_at) + VALUES (gen_random_uuid(), $1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING id` return p.conn.QueryRow( ctx, query, + item.UserID, item.Name, item.Category, item.Type, @@ -171,7 +186,7 @@ func (p *postgresInventoryRepository) CreateOrUpdate(ctx context.Context, item * date_added = $6, status = $7, updated_at = $8 - WHERE id = $9 + WHERE id = $9 AND user_id = $10 RETURNING id` return p.conn.QueryRow( @@ -186,11 +201,12 @@ func (p *postgresInventoryRepository) CreateOrUpdate(ctx context.Context, item * item.Status, 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 } From 54c8fd433c9af4f3ecf35627c713ab9d1d1243f1 Mon Sep 17 00:00:00 2001 From: Natthapol SERMSARAN Date: Tue, 1 Apr 2025 03:51:42 +0700 Subject: [PATCH 03/16] feat: add user to inventory domain --- backend/internal/domain/inventory.go | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/backend/internal/domain/inventory.go b/backend/internal/domain/inventory.go index ecd6169..c78923f 100644 --- a/backend/internal/domain/inventory.go +++ b/backend/internal/domain/inventory.go @@ -17,6 +17,7 @@ const ( type InventoryItem struct { ID string + UserID string Name string Category string Type string @@ -29,6 +30,7 @@ type InventoryItem struct { } type InventoryFilter struct { + UserID string Category string Type string Status InventoryStatus @@ -44,6 +46,7 @@ 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), @@ -54,8 +57,9 @@ func (i *InventoryItem) Validate() error { } 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 } From 0a1320805c4a9f08c73f48ca9ef8b5b5166a4207 Mon Sep 17 00:00:00 2001 From: Natthapol SERMSARAN Date: Tue, 1 Apr 2025 03:52:01 +0700 Subject: [PATCH 04/16] feat: add user to inventory and update api --- backend/internal/api/inventory.go | 49 ++++++++++++++++++------------- 1 file changed, 28 insertions(+), 21 deletions(-) diff --git a/backend/internal/api/inventory.go b/backend/internal/api/inventory.go index 206f6b3..78c95b4 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", @@ -50,8 +50,22 @@ func (a *api) registerInventoryRoutes(_ chi.Router, api huma.API) { }, a.deleteInventoryItemHandler) } +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 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"` @@ -71,6 +85,7 @@ type CreateInventoryItemOutput struct { func (a *api) createInventoryItemHandler(ctx context.Context, input *CreateInventoryItemInput) (*CreateInventoryItemOutput, error) { item := &domain.InventoryItem{ + UserID: input.UserID, Name: input.Body.Name, Category: input.Body.Category, Type: input.Body.Type, @@ -96,6 +111,7 @@ func (a *api) createInventoryItemHandler(ctx context.Context, input *CreateInven type GetInventoryItemsInput struct { Header string `header:"Authorization" required:"true" example:"Bearer token"` + UserID string `header:"user_id" required:"true" example:"user-uuid"` Category string `query:"category"` Type string `query:"type"` Status string `query:"status" enum:"In Stock,Low Stock,Out of Stock"` @@ -106,25 +122,13 @@ type GetInventoryItemsInput struct { 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{ + UserID: input.UserID, Category: input.Category, Type: input.Type, Status: domain.InventoryStatus(input.Status), @@ -138,7 +142,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 } @@ -164,6 +168,7 @@ func (a *api) getInventoryItemsHandler(ctx context.Context, input *GetInventoryI 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"` } @@ -172,7 +177,7 @@ type GetInventoryItemOutput struct { } 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 } @@ -193,6 +198,7 @@ func (a *api) getInventoryItemHandler(ctx context.Context, input *GetInventoryIt 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"` @@ -210,7 +216,7 @@ type UpdateInventoryItemOutput struct { } 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 } @@ -246,7 +252,7 @@ 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 } @@ -267,6 +273,7 @@ func (a *api) updateInventoryItemHandler(ctx context.Context, input *UpdateInven 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"` } @@ -277,7 +284,7 @@ type DeleteInventoryItemOutput struct { } 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 } From 4e42794aadcb14e92a955628d2fbf94ff0975109 Mon Sep 17 00:00:00 2001 From: Pattadon Date: Tue, 1 Apr 2025 10:47:22 +0700 Subject: [PATCH 05/16] feat: enhance EditInventoryItem component with props and status selection --- .../inventory/edit-inventory-item.tsx | 82 +++++++++++-------- frontend/app/(sidebar)/inventory/page.tsx | 16 ++-- 2 files changed, 56 insertions(+), 42 deletions(-) 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() { +
+ + +
-
- - - - - - - - - -