From 7a56e699534a85dd3a4473ea066709e0ea2d418f Mon Sep 17 00:00:00 2001 From: Natthapol SERMSARAN Date: Fri, 21 Mar 2025 22:58:31 +0700 Subject: [PATCH 01/14] ui: add delete inventoty item ui --- .../inventory/delete-inventory-item.tsx | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 frontend/app/(sidebar)/inventory/delete-inventory-item.tsx diff --git a/frontend/app/(sidebar)/inventory/delete-inventory-item.tsx b/frontend/app/(sidebar)/inventory/delete-inventory-item.tsx new file mode 100644 index 0000000..90036e7 --- /dev/null +++ b/frontend/app/(sidebar)/inventory/delete-inventory-item.tsx @@ -0,0 +1,59 @@ +"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(); + 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 ( + + ); +} From f2b2f0fcbea5373fff378b9a6c1971b0d8b74316 Mon Sep 17 00:00:00 2001 From: Natthapol SERMSARAN Date: Fri, 21 Mar 2025 22:59:24 +0700 Subject: [PATCH 02/14] ui: add edit inventoty item ui --- .../inventory/edit-inventory-item.tsx | 191 ++++++++++++++++++ 1 file changed, 191 insertions(+) create mode 100644 frontend/app/(sidebar)/inventory/edit-inventory-item.tsx diff --git a/frontend/app/(sidebar)/inventory/edit-inventory-item.tsx b/frontend/app/(sidebar)/inventory/edit-inventory-item.tsx new file mode 100644 index 0000000..0affb70 --- /dev/null +++ b/frontend/app/(sidebar)/inventory/edit-inventory-item.tsx @@ -0,0 +1,191 @@ +"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 function EditInventoryItem() { + const [date, setDate] = useState(); + 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 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 ( + + + + + + + Edit Inventory Item + + Edit a plantation or fertilizer item in your inventory. + + +
+
+ + setItemName(e.target.value)} + /> +
+
+ + +
+
+ + setItemCategory(e.target.value)} + /> +
+
+ + setItemQuantity(Number(e.target.value))} + /> +
+
+ + setItemUnit(e.target.value)} + /> +
+
+ + + + + + + + + +
+
+ + + +
+
+ ); +} From e0dc49499b7a0782671be8a22a8dd27eb0a07b5b Mon Sep 17 00:00:00 2001 From: Natthapol SERMSARAN Date: Fri, 21 Mar 2025 23:00:32 +0700 Subject: [PATCH 03/14] ui: add delete and edit button ui to inventoty page --- frontend/app/(sidebar)/inventory/page.tsx | 86 +++++++++++++++++++---- 1 file changed, 74 insertions(+), 12 deletions(-) diff --git a/frontend/app/(sidebar)/inventory/page.tsx b/frontend/app/(sidebar)/inventory/page.tsx index 290461b..3a9437e 100644 --- a/frontend/app/(sidebar)/inventory/page.tsx +++ b/frontend/app/(sidebar)/inventory/page.tsx @@ -6,15 +6,40 @@ import { Calendar, ChevronDown, Plus, Search } from "lucide-react"; 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 { + 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 { fetchInventoryItems } from "@/api/inventory"; import { AddInventoryItem } from "./add-inventory-item"; +import { EditInventoryItem } from "./edit-inventory-item"; +import { DeleteInventoryItem } from "./delete-inventory-item"; export default function InventoryPage() { const [date, setDate] = useState(); @@ -33,12 +58,18 @@ export default function InventoryPage() { }); if (isLoading) { - return
Loading...
; + return ( +
+ Loading... +
+ ); } if (isError || !inventoryItems) { return ( -
Error loading inventory data.
+
+ Error loading inventory data. +
); } @@ -47,7 +78,9 @@ export default function InventoryPage() { inventoryType === "all" ? inventoryItems : inventoryItems.filter((item) => - inventoryType === "plantation" ? item.type === "Plantation" : item.type === "Fertilizer" + inventoryType === "plantation" + ? item.type === "Plantation" + : item.type === "Fertilizer", ); return ( @@ -62,7 +95,8 @@ export default function InventoryPage() { + @@ -116,12 +159,17 @@ export default function InventoryPage() { Quantity Last Updated Status + Edit + Delete {filteredItems.length === 0 ? ( - + No inventory items found @@ -136,7 +184,21 @@ export default function InventoryPage() { {item.lastUpdated} - {item.status} + + {item.status} + + + + + + + )) From 044a2856cf4fbe9618d62f8f621b90eef4ab0f97 Mon Sep 17 00:00:00 2001 From: Natthapol SERMSARAN Date: Sat, 29 Mar 2025 19:11:22 +0700 Subject: [PATCH 04/14] feat: add inventory domain --- backend/internal/domain/inventory.go | 61 ++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 backend/internal/domain/inventory.go diff --git a/backend/internal/domain/inventory.go b/backend/internal/domain/inventory.go new file mode 100644 index 0000000..ecd6169 --- /dev/null +++ b/backend/internal/domain/inventory.go @@ -0,0 +1,61 @@ +package domain + +import ( + "context" + "time" + + validation "github.com/go-ozzo/ozzo-validation/v4" +) + +type InventoryStatus string + +const ( + StatusInStock InventoryStatus = "In Stock" + StatusLowStock InventoryStatus = "Low Stock" + StatusOutOfStock InventoryStatus = "Out of Stock" +) + +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 +} + +type InventoryFilter struct { + Category string + Type string + Status InventoryStatus + 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.Name, validation.Required), + validation.Field(&i.Category, validation.Required), + validation.Field(&i.Type, 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)), + ) +} + +type InventoryRepository interface { + GetByID(ctx context.Context, id string) (InventoryItem, error) + GetWithFilter(ctx context.Context, filter InventoryFilter, sort InventorySort) ([]InventoryItem, error) + CreateOrUpdate(ctx context.Context, item *InventoryItem) error + Delete(ctx context.Context, id string) error +} From 4ed46f9ba586a9a6c032208f509267c361697a16 Mon Sep 17 00:00:00 2001 From: Natthapol SERMSARAN Date: Sat, 29 Mar 2025 19:12:46 +0700 Subject: [PATCH 05/14] feat: add inventory repository for query logic --- .../internal/repository/postgres_inventory.go | 196 ++++++++++++++++++ 1 file changed, 196 insertions(+) create mode 100644 backend/internal/repository/postgres_inventory.go diff --git a/backend/internal/repository/postgres_inventory.go b/backend/internal/repository/postgres_inventory.go new file mode 100644 index 0000000..1a05655 --- /dev/null +++ b/backend/internal/repository/postgres_inventory.go @@ -0,0 +1,196 @@ +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.Name, + &i.Category, + &i.Type, + &i.Quantity, + &i.Unit, + &i.DateAdded, + &i.Status, + &i.CreatedAt, + &i.UpdatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + return items, nil +} + +func (p *postgresInventoryRepository) GetByID(ctx context.Context, id 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` + + items, err := p.fetch(ctx, query, id) + if err != nil { + return domain.InventoryItem{}, err + } + if len(items) == 0 { + return domain.InventoryItem{}, domain.ErrNotFound + } + return items[0], nil +} + +func (p *postgresInventoryRepository) GetWithFilter(ctx context.Context, filter domain.InventoryFilter, sort domain.InventorySort) ([]domain.InventoryItem, error) { + var query strings.Builder + args := []interface{}{} + argPos := 1 + + query.WriteString(` + SELECT id, name, category, type, quantity, unit, date_added, status, created_at, updated_at + FROM inventory_items + WHERE 1=1`) + + if filter.Category != "" { + query.WriteString(fmt.Sprintf(" AND category = $%d", argPos)) + args = append(args, filter.Category) + 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) + argPos++ + } + + if !filter.StartDate.IsZero() { + query.WriteString(fmt.Sprintf(" AND date_added >= $%d", argPos)) + args = append(args, filter.StartDate) + argPos++ + } + + if !filter.EndDate.IsZero() { + query.WriteString(fmt.Sprintf(" AND date_added <= $%d", argPos)) + args = append(args, filter.EndDate) + argPos++ + } + + if filter.SearchQuery != "" { + query.WriteString(fmt.Sprintf(" AND name ILIKE $%d", argPos)) + args = append(args, "%"+filter.SearchQuery+"%") + argPos++ + } + + if sort.Field == "" { + sort.Field = "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, + } + + 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") + } + } + + return p.fetch(ctx, query.String(), args...) +} + +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, 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) + RETURNING id` + return p.conn.QueryRow( + ctx, + query, + item.Name, + item.Category, + item.Type, + item.Quantity, + item.Unit, + item.DateAdded, + item.Status, + item.CreatedAt, + item.UpdatedAt, + ).Scan(&item.ID) + } + + 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 + RETURNING id` + + return p.conn.QueryRow( + ctx, + query, + item.Name, + item.Category, + item.Type, + item.Quantity, + item.Unit, + item.DateAdded, + item.Status, + item.UpdatedAt, + item.ID, + ).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) + return err +} From 64098b5d1012fd2dd9818543d1bc0162c1824135 Mon Sep 17 00:00:00 2001 From: Natthapol SERMSARAN Date: Sat, 29 Mar 2025 19:13:15 +0700 Subject: [PATCH 06/14] feat: add inventory api --- backend/internal/api/inventory.go | 288 ++++++++++++++++++++++++++++++ 1 file changed, 288 insertions(+) create mode 100644 backend/internal/api/inventory.go diff --git a/backend/internal/api/inventory.go b/backend/internal/api/inventory.go new file mode 100644 index 0000000..206f6b3 --- /dev/null +++ b/backend/internal/api/inventory.go @@ -0,0 +1,288 @@ +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: "getInventoryItems", + Method: http.MethodGet, + Path: prefix, + Tags: tags, + }, a.getInventoryItemsHandler) + + 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) +} + +type CreateInventoryItemInput struct { + Header string `header:"Authorization" required:"true" example:"Bearer token"` + 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"` + } +} + +type CreateInventoryItemOutput struct { + Body struct { + ID string `json:"id"` + } +} + +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), + } + + 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 +} + +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) { + filter := domain.InventoryFilter{ + Category: input.Category, + Type: input.Type, + Status: domain.InventoryStatus(input.Status), + StartDate: input.StartDate, + EndDate: input.EndDate, + SearchQuery: input.SearchQuery, + } + + sort := domain.InventorySort{ + Field: input.SortBy, + Direction: input.SortOrder, + } + + items, err := a.inventoryRepo.GetWithFilter(ctx, 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: item.Category, + Type: item.Type, + Quantity: item.Quantity, + Unit: item.Unit, + DateAdded: item.DateAdded, + Status: string(item.Status), + CreatedAt: item.CreatedAt, + UpdatedAt: item.UpdatedAt, + } + } + + 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) + 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, + DateAdded: item.DateAdded, + Status: string(item.Status), + 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) + if err != nil { + return nil, err + } + + 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.Quantity != 0 { + item.Quantity = input.Body.Quantity + } + if input.Body.Unit != "" { + item.Unit = input.Body.Unit + } + if !input.Body.DateAdded.IsZero() { + item.DateAdded = input.Body.DateAdded + } + if input.Body.Status != "" { + item.Status = domain.InventoryStatus(input.Body.Status) + } + + 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) + 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, + DateAdded: updatedItem.DateAdded, + Status: string(updatedItem.Status), + 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) + if err != nil { + return nil, err + } + + return &DeleteInventoryItemOutput{Body: struct { + Message string `json:"message"` + }{Message: "Inventory item deleted successfully"}}, nil +} From 860181d98308710fc1e96ec470b393d7d1216330 Mon Sep 17 00:00:00 2001 From: Natthapol SERMSARAN Date: Sat, 29 Mar 2025 19:16:51 +0700 Subject: [PATCH 07/14] feat: add inventory routes and handlers to api.go --- backend/internal/api/api.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/backend/internal/api/api.go b/backend/internal/api/api.go index 3f9b560..3e1b67a 100644 --- a/backend/internal/api/api.go +++ b/backend/internal/api/api.go @@ -26,6 +26,7 @@ type api struct { cropRepo domain.CroplandRepository farmRepo domain.FarmRepository plantRepo domain.PlantRepository + inventoryRepo domain.InventoryRepository } func NewAPI(ctx context.Context, logger *slog.Logger, pool *pgxpool.Pool) *api { @@ -36,6 +37,7 @@ func NewAPI(ctx context.Context, logger *slog.Logger, pool *pgxpool.Pool) *api { croplandRepository := repository.NewPostgresCropland(pool) farmRepository := repository.NewPostgresFarm(pool) plantRepository := repository.NewPostgresPlant(pool) + inventoryRepository := repository.NewPostgresInventory(pool) return &api{ logger: logger, @@ -45,6 +47,7 @@ func NewAPI(ctx context.Context, logger *slog.Logger, pool *pgxpool.Pool) *api { cropRepo: croplandRepository, farmRepo: farmRepository, plantRepo: plantRepository, + inventoryRepo: inventoryRepository, } } @@ -84,6 +87,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 From 41e5b1fb816fd004e8a62fb7c4b431d1552aaa09 Mon Sep 17 00:00:00 2001 From: Natthapol SERMSARAN Date: Sat, 29 Mar 2025 19:17:46 +0700 Subject: [PATCH 08/14] feat: create inventory_items table migration --- .../00004_create_inventory_items_table.sql | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 backend/migrations/00004_create_inventory_items_table.sql diff --git a/backend/migrations/00004_create_inventory_items_table.sql b/backend/migrations/00004_create_inventory_items_table.sql new file mode 100644 index 0000000..eaf6945 --- /dev/null +++ b/backend/migrations/00004_create_inventory_items_table.sql @@ -0,0 +1,16 @@ +-- +goose Up +CREATE TABLE inventory_items ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + 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() +); + +CREATE INDEX idx_inventory_items_category ON inventory_items(category); +CREATE INDEX idx_inventory_items_status ON inventory_items(status); From 2b694a1b44a0d6459474b927bac5d9d2cb60b499 Mon Sep 17 00:00:00 2001 From: THIS ONE IS A LITTLE BIT TRICKY KRUB Date: Mon, 31 Mar 2025 21:07:23 +0700 Subject: [PATCH 09/14] change to use the tannstack table --- .../farms/[farmId]/crops/[cropId]/page.tsx | 141 ++++++-- frontend/app/(sidebar)/inventory/page.tsx | 300 +++++++++--------- frontend/package.json | 1 + frontend/pnpm-lock.yaml | 20 ++ 4 files changed, 282 insertions(+), 180 deletions(-) diff --git a/frontend/app/(sidebar)/farms/[farmId]/crops/[cropId]/page.tsx b/frontend/app/(sidebar)/farms/[farmId]/crops/[cropId]/page.tsx index 0b767fd..4c444ab 100644 --- a/frontend/app/(sidebar)/farms/[farmId]/crops/[cropId]/page.tsx +++ b/frontend/app/(sidebar)/farms/[farmId]/crops/[cropId]/page.tsx @@ -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 }) { +export default function CropDetailPage({ + params, +}: { + params: Promise; +}) { const router = useRouter(); const [crop, setCrop] = useState(null); const [analytics, setAnalytics] = useState(null); @@ -57,7 +71,9 @@ export default function CropDetailPage({ params }: { params: PromiseLoading... +
+ Loading... +
); } @@ -87,7 +103,8 @@ export default function CropDetailPage({ params }: { params: Promise 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 router.back()}> + onClick={() => router.back()} + > Back to Farm @@ -126,7 +144,9 @@ export default function CropDetailPage({ params }: { params: Promise

Growth Timeline

-

Planted on {crop.plantedDate.toLocaleDateString()}

+

+ Planted on {crop.plantedDate.toLocaleDateString()} +

@@ -150,19 +170,28 @@ export default function CropDetailPage({ params }: { params: Promise
- + Health Score: {crop.healthScore}% - + Growing
{crop.expectedHarvest ? (

- Expected harvest: {crop.expectedHarvest.toLocaleDateString()} + Expected harvest:{" "} + {crop.expectedHarvest.toLocaleDateString()}

) : ( -

Expected harvest date not available

+

+ Expected harvest date not available +

)}
@@ -180,13 +209,18 @@ export default function CropDetailPage({ params }: { params: Promise -
+ onClick={action.onClick} + > +
{action.title}
-

{action.description}

+

+ {action.description} +

))} @@ -196,7 +230,9 @@ export default function CropDetailPage({ params }: { params: Promise Environmental Conditions - Real-time monitoring of growing conditions + + Real-time monitoring of growing conditions +
@@ -247,15 +283,22 @@ export default function CropDetailPage({ params }: { params: Promise ( + className="border-none shadow-none bg-gradient-to-br from-white to-gray-50/50 dark:from-slate-800 dark:to-slate-700/50" + >
- +
-

{metric.label}

-

{metric.value}

+

+ {metric.label} +

+

+ {metric.value} +

@@ -269,9 +312,14 @@ export default function CropDetailPage({ params }: { params: Promise
Growth Progress - {analytics.growthProgress}% + + {analytics.growthProgress}% +
- +
{/* Next Action Card */} @@ -282,10 +330,15 @@ export default function CropDetailPage({ params }: { params: Promise
-

Next Action Required

-

{analytics.nextAction}

+

+ Next Action Required +

+

+ {analytics.nextAction} +

- Due by {analytics.nextActionDue.toLocaleDateString()} + Due by{" "} + {analytics.nextActionDue.toLocaleDateString()}

@@ -337,9 +390,14 @@ export default function CropDetailPage({ params }: { params: Promise
{nutrient.name} - {nutrient.value}% + + {nutrient.value}% +
- + ))} @@ -372,10 +430,14 @@ export default function CropDetailPage({ params }: { params: Promise -

2 hours ago

+

+ 2 hours ago +

- {i < 4 && } + {i < 4 && ( + + )} ))} @@ -385,8 +447,17 @@ export default function CropDetailPage({ params }: { params: Promise {/* Dialogs */} - - + + ); @@ -399,9 +470,15 @@ function Activity({ icon }: { icon: number }) { const icons = [ , , - , + , , - , + , ]; return icons[icon]; } diff --git a/frontend/app/(sidebar)/inventory/page.tsx b/frontend/app/(sidebar)/inventory/page.tsx index 3a9437e..9b02fc9 100644 --- a/frontend/app/(sidebar)/inventory/page.tsx +++ b/frontend/app/(sidebar)/inventory/page.tsx @@ -1,19 +1,25 @@ "use client"; -import { useState } from "react"; +import { + JSXElementConstructor, + ReactElement, + ReactNode, + ReactPortal, + useState, +} 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, @@ -26,14 +32,8 @@ 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 { fetchInventoryItems } from "@/api/inventory"; @@ -44,11 +44,14 @@ import { DeleteInventoryItem } from "./delete-inventory-item"; export default function InventoryPage() { const [date, setDate] = useState(); const [inventoryType, setInventoryType] = useState("all"); - const [currentPage, setCurrentPage] = useState(1); + const [sorting, setSorting] = useState([]); + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 10, + }); - // Fetch inventory items using react-query. const { - data: inventoryItems, + data: inventoryItems = [], isLoading, isError, } = useQuery({ @@ -57,155 +60,156 @@ export default function InventoryPage() { staleTime: 60 * 1000, }); - if (isLoading) { - return ( -
- Loading... -
- ); - } - - if (isError || !inventoryItems) { - return ( -
- Error loading inventory data. -
- ); - } - - // Filter items based on selected type. const filteredItems = inventoryType === "all" ? inventoryItems - : inventoryItems.filter((item) => - inventoryType === "plantation" - ? item.type === "Plantation" - : item.type === "Fertilizer", + : inventoryItems.filter( + (item) => item.type.toLowerCase() === inventoryType ); + const columns = [ + { accessorKey: "name", header: "Name" }, + { accessorKey: "category", header: "Category" }, + { accessorKey: "type", header: "Type" }, + { accessorKey: "quantity", header: "Quantity" }, + { accessorKey: "lastUpdated", header: "Last Updated" }, + { + accessorKey: "status", + header: "Status", + cell: (info: { + getValue: () => + | string + | number + | bigint + | boolean + | ReactElement> + | Iterable + | ReactPortal + | Promise< + | string + | number + | bigint + | boolean + | ReactPortal + | ReactElement> + | Iterable + | null + | undefined + > + | null + | undefined; + }) => {info.getValue()}, + }, + { accessorKey: "edit", header: "Edit", cell: () => }, + { + accessorKey: "delete", + header: "Delete", + cell: () => , + }, + ]; + + const table = useReactTable({ + data: filteredItems, + columns, + state: { sorting, pagination }, + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + getPaginationRowModel: getPaginationRowModel(), + onSortingChange: setSorting, + onPaginationChange: setPagination, + }); + + if (isLoading) + return ( +
+ Loading... +
+ ); + if (isError) + return ( +
+ Error loading inventory data. +
+ ); + return (

Inventory

- - {/* Filters and search */}
-
- - -
- -
- - - - - - - - - -
- - -
- - -
+ + +
- - {/* Table */}
-

Table Fields

- - Name - Category - Type - Quantity - Last Updated - Status - Edit - Delete - + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + {flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ))} + + ))} - {filteredItems.length === 0 ? ( - - - No inventory items found - + {table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ))} - ) : ( - filteredItems.map((item) => ( - - {item.name} - {item.category} - {item.type} - - {item.quantity} {item.unit} - - {item.lastUpdated} - - - {item.status} - - - - - - - - - - )) - )} + ))}
+ + + + + + + + + + +
diff --git a/frontend/package.json b/frontend/package.json index 4da49cc..6dd6b31 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 7635441..f09642c 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -71,6 +71,9 @@ dependencies: '@tanstack/react-query': specifier: ^5.66.0 version: 5.67.3(react@19.0.0) + '@tanstack/react-table': + specifier: ^8.21.2 + version: 8.21.2(react-dom@19.0.0)(react@19.0.0) axios: specifier: ^1.7.9 version: 1.8.3 @@ -1600,6 +1603,23 @@ packages: react: 19.0.0 dev: false + /@tanstack/react-table@8.21.2(react-dom@19.0.0)(react@19.0.0): + resolution: {integrity: sha512-11tNlEDTdIhMJba2RBH+ecJ9l1zgS2kjmexDPAraulc8jeNA4xocSNeyzextT0XJyASil4XsCYlJmf5jEWAtYg==} + engines: {node: '>=12'} + peerDependencies: + react: '>=16.8' + react-dom: '>=16.8' + dependencies: + '@tanstack/table-core': 8.21.2 + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + dev: false + + /@tanstack/table-core@8.21.2: + resolution: {integrity: sha512-uvXk/U4cBiFMxt+p9/G7yUWI/UbHYbyghLCjlpWZ3mLeIZiUBSKcUnw9UnKkdRz7Z/N4UBuFLWQdJCjUe7HjvA==} + engines: {node: '>=12'} + dev: false + /@types/d3-array@3.2.1: resolution: {integrity: sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==} dev: false From 83bffe974739f8d100f0ef31be530cb793040f5e Mon Sep 17 00:00:00 2001 From: THIS ONE IS A LITTLE BIT TRICKY KRUB Date: Mon, 31 Mar 2025 21:37:38 +0700 Subject: [PATCH 10/14] feat: add search functionality to inventory page --- frontend/app/(sidebar)/inventory/page.tsx | 42 ++++++++++++++--------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/frontend/app/(sidebar)/inventory/page.tsx b/frontend/app/(sidebar)/inventory/page.tsx index 9b02fc9..ff857f7 100644 --- a/frontend/app/(sidebar)/inventory/page.tsx +++ b/frontend/app/(sidebar)/inventory/page.tsx @@ -1,11 +1,12 @@ "use client"; import { + useState, JSXElementConstructor, ReactElement, ReactNode, ReactPortal, - useState, + useMemo, } from "react"; import { useQuery } from "@tanstack/react-query"; import { @@ -33,17 +34,15 @@ import { PaginationContent, PaginationItem, } from "@/components/ui/pagination"; +import { Search } from "lucide-react"; import { Badge } from "@/components/ui/badge"; - import { fetchInventoryItems } from "@/api/inventory"; import { AddInventoryItem } from "./add-inventory-item"; import { EditInventoryItem } from "./edit-inventory-item"; import { DeleteInventoryItem } from "./delete-inventory-item"; export default function InventoryPage() { - const [date, setDate] = useState(); - const [inventoryType, setInventoryType] = useState("all"); const [sorting, setSorting] = useState([]); const [pagination, setPagination] = useState({ pageIndex: 0, @@ -59,13 +58,16 @@ export default function InventoryPage() { queryFn: fetchInventoryItems, staleTime: 60 * 1000, }); - - const filteredItems = - inventoryType === "all" - ? inventoryItems - : inventoryItems.filter( - (item) => item.type.toLowerCase() === inventoryType - ); + const [searchTerm, setSearchTerm] = useState(""); + const handleSearch = () => { + // update search state when user clicks or presses enter + setSearchTerm(searchTerm); + }; + const filteredItems = useMemo(() => { + return inventoryItems.filter((item) => + item.name.toLowerCase().includes(searchTerm.toLowerCase()) + ); + }, [inventoryItems, searchTerm]); const columns = [ { accessorKey: "name", header: "Name" }, @@ -138,10 +140,13 @@ export default function InventoryPage() {

Inventory

- - + + setSearchTerm(e.target.value)} + />
@@ -180,7 +185,7 @@ export default function InventoryPage() {
- + @@ -204,6 +210,10 @@ export default function InventoryPage() { pageIndex: prev.pageIndex + 1, })) } + disabled={ + pagination.pageIndex >= + Math.ceil(filteredItems.length / pagination.pageSize) - 1 + } > Next From e004c62941e5e04655e70f179d19f5a5920258fb Mon Sep 17 00:00:00 2001 From: THIS ONE IS A LITTLE BIT TRICKY KRUB Date: Mon, 31 Mar 2025 21:53:09 +0700 Subject: [PATCH 11/14] feat: add sorting icons to inventory table headers and disable sorting for edit/delete columns --- frontend/app/(sidebar)/inventory/page.tsx | 32 +++++++++++++++++++---- frontend/package.json | 1 + frontend/pnpm-lock.yaml | 11 ++++++++ 3 files changed, 39 insertions(+), 5 deletions(-) diff --git a/frontend/app/(sidebar)/inventory/page.tsx b/frontend/app/(sidebar)/inventory/page.tsx index ff857f7..ed25255 100644 --- a/frontend/app/(sidebar)/inventory/page.tsx +++ b/frontend/app/(sidebar)/inventory/page.tsx @@ -35,6 +35,7 @@ import { PaginationItem, } from "@/components/ui/pagination"; import { Search } from "lucide-react"; +import { FaSort, FaSortUp, FaSortDown } from "react-icons/fa"; import { Badge } from "@/components/ui/badge"; import { fetchInventoryItems } from "@/api/inventory"; @@ -102,11 +103,17 @@ export default function InventoryPage() { | undefined; }) => {info.getValue()}, }, - { accessorKey: "edit", header: "Edit", cell: () => }, + { + accessorKey: "edit", + header: "Edit", + cell: () => , + enableSorting: false, + }, { accessorKey: "delete", header: "Delete", cell: () => , + enableSorting: false, }, ]; @@ -158,11 +165,26 @@ export default function InventoryPage() { - {flexRender( - header.column.columnDef.header, - header.getContext() - )} +
+ {flexRender( + header.column.columnDef.header, + header.getContext() + )} + {header.column.getCanSort() && + !header.column.columnDef.enableSorting && ( + + {header.column.getIsSorted() === "desc" ? ( + + ) : header.column.getIsSorted() === "asc" ? ( + + ) : ( + + )} + + )} +
))} diff --git a/frontend/package.json b/frontend/package.json index 6dd6b31..a1c4030 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -46,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", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index f09642c..5b587b5 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -116,6 +116,9 @@ dependencies: react-hook-form: specifier: ^7.54.2 version: 7.54.2(react@19.0.0) + react-icons: + specifier: ^5.5.0 + version: 5.5.0(react@19.0.0) recharts: specifier: ^2.15.1 version: 2.15.1(react-dom@19.0.0)(react@19.0.0) @@ -3996,6 +3999,14 @@ packages: react: 19.0.0 dev: false + /react-icons@5.5.0(react@19.0.0): + resolution: {integrity: sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==} + peerDependencies: + react: '*' + dependencies: + react: 19.0.0 + dev: false + /react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} From 0df2d8241ef00ef71ab8f1049125496d520d9404 Mon Sep 17 00:00:00 2001 From: THIS ONE IS A LITTLE BIT TRICKY KRUB Date: Mon, 31 Mar 2025 21:58:12 +0700 Subject: [PATCH 12/14] feat: enhance status display in inventory table with conditional styling --- frontend/app/(sidebar)/inventory/page.tsx | 41 ++++++++++------------- 1 file changed, 18 insertions(+), 23 deletions(-) diff --git a/frontend/app/(sidebar)/inventory/page.tsx b/frontend/app/(sidebar)/inventory/page.tsx index ed25255..b93c003 100644 --- a/frontend/app/(sidebar)/inventory/page.tsx +++ b/frontend/app/(sidebar)/inventory/page.tsx @@ -59,6 +59,7 @@ export default function InventoryPage() { queryFn: fetchInventoryItems, staleTime: 60 * 1000, }); + // console.table(inventoryItems); const [searchTerm, setSearchTerm] = useState(""); const handleSearch = () => { // update search state when user clicks or presses enter @@ -79,29 +80,23 @@ export default function InventoryPage() { { accessorKey: "status", header: "Status", - cell: (info: { - getValue: () => - | string - | number - | bigint - | boolean - | ReactElement> - | Iterable - | ReactPortal - | Promise< - | string - | number - | bigint - | boolean - | ReactPortal - | ReactElement> - | Iterable - | null - | undefined - > - | null - | undefined; - }) => {info.getValue()}, + cell: (info: { getValue: () => string }) => { + const status = info.getValue(); + + 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 ( + + {status} + + ); + }, }, { accessorKey: "edit", From 486c16ab1550b938fc5dad796a40d3f175bae546 Mon Sep 17 00:00:00 2001 From: THIS ONE IS A LITTLE BIT TRICKY KRUB Date: Mon, 31 Mar 2025 22:12:18 +0700 Subject: [PATCH 13/14] feat: style delete button and remove unused search handler in inventory page --- frontend/app/(sidebar)/inventory/delete-inventory-item.tsx | 6 +++++- frontend/app/(sidebar)/inventory/page.tsx | 4 ---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/frontend/app/(sidebar)/inventory/delete-inventory-item.tsx b/frontend/app/(sidebar)/inventory/delete-inventory-item.tsx index 90036e7..e53e37d 100644 --- a/frontend/app/(sidebar)/inventory/delete-inventory-item.tsx +++ b/frontend/app/(sidebar)/inventory/delete-inventory-item.tsx @@ -52,7 +52,11 @@ export function DeleteInventoryItem() { }; return ( - ); diff --git a/frontend/app/(sidebar)/inventory/page.tsx b/frontend/app/(sidebar)/inventory/page.tsx index b93c003..792f3a1 100644 --- a/frontend/app/(sidebar)/inventory/page.tsx +++ b/frontend/app/(sidebar)/inventory/page.tsx @@ -61,10 +61,6 @@ export default function InventoryPage() { }); // console.table(inventoryItems); const [searchTerm, setSearchTerm] = useState(""); - const handleSearch = () => { - // update search state when user clicks or presses enter - setSearchTerm(searchTerm); - }; const filteredItems = useMemo(() => { return inventoryItems.filter((item) => item.name.toLowerCase().includes(searchTerm.toLowerCase()) From fd00ec02de2c29e3e383bff3c4fb5da1feae633c Mon Sep 17 00:00:00 2001 From: THIS ONE IS A LITTLE BIT TRICKY KRUB Date: Mon, 31 Mar 2025 22:16:14 +0700 Subject: [PATCH 14/14] refactor: format axios post request for better readability in createInventoryItem function --- frontend/api/inventory.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frontend/api/inventory.ts b/frontend/api/inventory.ts index 7d8982d..69fd6b3 100644 --- a/frontend/api/inventory.ts +++ b/frontend/api/inventory.ts @@ -79,7 +79,10 @@ export async function createInventoryItem( // Simulate network delay await new Promise((resolve) => setTimeout(resolve, 500)); try { - const response = await axiosInstance.post("/api/inventory", item); + const response = await axiosInstance.post( + "/api/inventory", + item + ); return response.data; } catch (error) { // Simulate successful creation if API endpoint is not available