From fc0eb6d2be5d98fca6a289691245e175a894f86d Mon Sep 17 00:00:00 2001 From: Forbes Date: Sat, 31 Jan 2026 14:27:11 -0600 Subject: [PATCH] feat: add sourcing type, extended fields, and inline project tagging - Add migration 009: sourcing_type (manufactured/purchased), sourcing_link, long_description, and standard_cost columns on items table - Update Item struct, repository queries, and API handlers for new fields - Add sourcing badge, long description block, standard cost, and sourcing link display to item detail panel - Add inline project tag editor in detail panel (add/remove via dropdown) - Add new fields to create and edit modals - Update CSV import/export for new columns - Merge with auth CreatedBy/UpdatedBy changes from stash --- internal/api/csv.go | 38 ++++ internal/api/handlers.go | 71 ++++-- internal/api/templates/items.html | 282 +++++++++++++++++++++++- internal/db/items.go | 63 +++++- internal/db/projects.go | 4 +- migrations/009_item_extended_fields.sql | 21 ++ 6 files changed, 431 insertions(+), 48 deletions(-) create mode 100644 migrations/009_item_extended_fields.sql diff --git a/internal/api/csv.go b/internal/api/csv.go index 8ce63a8..30b48c9 100644 --- a/internal/api/csv.go +++ b/internal/api/csv.go @@ -50,6 +50,10 @@ var csvColumns = []string{ "updated_at", "category", "projects", // comma-separated project codes + "sourcing_type", + "sourcing_link", + "long_description", + "standard_cost", } // HandleExportCSV exports items to CSV format. @@ -153,6 +157,16 @@ func (s *Server) HandleExportCSV(w http.ResponseWriter, r *http.Request) { row[5] = item.UpdatedAt.Format(time.RFC3339) row[6] = category row[7] = projectCodes + row[8] = item.SourcingType + if item.SourcingLink != nil { + row[9] = *item.SourcingLink + } + if item.LongDescription != nil { + row[10] = *item.LongDescription + } + if item.StandardCost != nil { + row[11] = strconv.FormatFloat(*item.StandardCost, 'f', -1, 64) + } // Property columns if includeProps { @@ -350,6 +364,12 @@ func (s *Server) HandleImportCSV(w http.ResponseWriter, r *http.Request) { } } + // Parse extended fields + sourcingType := getCSVValue(record, colIndex, "sourcing_type") + sourcingLink := getCSVValue(record, colIndex, "sourcing_link") + longDesc := getCSVValue(record, colIndex, "long_description") + stdCostStr := getCSVValue(record, colIndex, "standard_cost") + // Create item item := &db.Item{ PartNumber: partNumber, @@ -359,6 +379,20 @@ func (s *Server) HandleImportCSV(w http.ResponseWriter, r *http.Request) { if user := auth.UserFromContext(ctx); user != nil { item.CreatedBy = &user.Username } + if sourcingType != "" { + item.SourcingType = sourcingType + } + if sourcingLink != "" { + item.SourcingLink = &sourcingLink + } + if longDesc != "" { + item.LongDescription = &longDesc + } + if stdCostStr != "" { + if cost, err := strconv.ParseFloat(stdCostStr, 64); err == nil { + item.StandardCost = &cost + } + } if err := s.items.Create(ctx, item, properties); err != nil { result.Errors = append(result.Errors, CSVImportErr{ @@ -543,6 +577,10 @@ func isStandardColumn(col string) bool { "projects": true, "objects": true, // FreeCAD objects data - skip on import "archived_at": true, + "sourcing_type": true, + "sourcing_link": true, + "long_description": true, + "standard_cost": true, } return standardCols[col] } diff --git a/internal/api/handlers.go b/internal/api/handlers.go index bc8103a..6347a86 100644 --- a/internal/api/handlers.go +++ b/internal/api/handlers.go @@ -232,16 +232,24 @@ type ItemResponse struct { CurrentRevision int `json:"current_revision"` CreatedAt string `json:"created_at"` UpdatedAt string `json:"updated_at"` + SourcingType string `json:"sourcing_type"` + SourcingLink *string `json:"sourcing_link,omitempty"` + LongDescription *string `json:"long_description,omitempty"` + StandardCost *float64 `json:"standard_cost,omitempty"` Properties map[string]any `json:"properties,omitempty"` } // CreateItemRequest represents a request to create an item. type CreateItemRequest struct { - Schema string `json:"schema"` - Category string `json:"category"` - Description string `json:"description"` - Projects []string `json:"projects,omitempty"` - Properties map[string]any `json:"properties,omitempty"` + Schema string `json:"schema"` + Category string `json:"category"` + Description string `json:"description"` + Projects []string `json:"projects,omitempty"` + Properties map[string]any `json:"properties,omitempty"` + SourcingType string `json:"sourcing_type,omitempty"` + SourcingLink *string `json:"sourcing_link,omitempty"` + LongDescription *string `json:"long_description,omitempty"` + StandardCost *float64 `json:"standard_cost,omitempty"` } // HandleListItems lists items with optional filtering. @@ -370,9 +378,13 @@ func (s *Server) HandleCreateItem(w http.ResponseWriter, r *http.Request) { // Create item item := &db.Item{ - PartNumber: partNumber, - ItemType: itemType, - Description: req.Description, + PartNumber: partNumber, + ItemType: itemType, + Description: req.Description, + SourcingType: req.SourcingType, + SourcingLink: req.SourcingLink, + LongDescription: req.LongDescription, + StandardCost: req.StandardCost, } if user := auth.UserFromContext(ctx); user != nil { item.CreatedBy = &user.Username @@ -439,11 +451,15 @@ func (s *Server) HandleGetItem(w http.ResponseWriter, r *http.Request) { // UpdateItemRequest represents a request to update an item. type UpdateItemRequest struct { - PartNumber string `json:"part_number,omitempty"` - ItemType string `json:"item_type,omitempty"` - Description string `json:"description,omitempty"` - Properties map[string]any `json:"properties,omitempty"` - Comment string `json:"comment,omitempty"` + PartNumber string `json:"part_number,omitempty"` + ItemType string `json:"item_type,omitempty"` + Description string `json:"description,omitempty"` + Properties map[string]any `json:"properties,omitempty"` + Comment string `json:"comment,omitempty"` + SourcingType *string `json:"sourcing_type,omitempty"` + SourcingLink *string `json:"sourcing_link,omitempty"` + LongDescription *string `json:"long_description,omitempty"` + StandardCost *float64 `json:"standard_cost,omitempty"` } // HandleUpdateItem updates an item's fields and/or creates a new revision. @@ -469,26 +485,31 @@ func (s *Server) HandleUpdateItem(w http.ResponseWriter, r *http.Request) { } // Update item fields if provided - newPartNumber := item.PartNumber - newItemType := item.ItemType - newDescription := item.Description + fields := db.UpdateItemFields{ + PartNumber: item.PartNumber, + ItemType: item.ItemType, + Description: item.Description, + SourcingType: req.SourcingType, + SourcingLink: req.SourcingLink, + LongDescription: req.LongDescription, + StandardCost: req.StandardCost, + } if req.PartNumber != "" { - newPartNumber = req.PartNumber + fields.PartNumber = req.PartNumber } if req.ItemType != "" { - newItemType = req.ItemType + fields.ItemType = req.ItemType } if req.Description != "" { - newDescription = req.Description + fields.Description = req.Description } // Update the item record (UUID stays the same) - var updatedBy *string if user := auth.UserFromContext(ctx); user != nil { - updatedBy = &user.Username + fields.UpdatedBy = &user.Username } - if err := s.items.Update(ctx, item.ID, newPartNumber, newItemType, newDescription, updatedBy); err != nil { + if err := s.items.Update(ctx, item.ID, fields); err != nil { s.logger.Error().Err(err).Msg("failed to update item") writeError(w, http.StatusInternalServerError, "update_failed", err.Error()) return @@ -513,7 +534,7 @@ func (s *Server) HandleUpdateItem(w http.ResponseWriter, r *http.Request) { } // Get updated item (use new part number if changed) - item, _ = s.items.GetByPartNumber(ctx, newPartNumber) + item, _ = s.items.GetByPartNumber(ctx, fields.PartNumber) writeJSON(w, http.StatusOK, itemToResponse(item)) } @@ -1074,6 +1095,10 @@ func itemToResponse(item *db.Item) ItemResponse { CurrentRevision: item.CurrentRevision, CreatedAt: item.CreatedAt.Format("2006-01-02T15:04:05Z07:00"), UpdatedAt: item.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"), + SourcingType: item.SourcingType, + SourcingLink: item.SourcingLink, + LongDescription: item.LongDescription, + StandardCost: item.StandardCost, } } diff --git a/internal/api/templates/items.html b/internal/api/templates/items.html index 0b7be81..c532d4f 100644 --- a/internal/api/templates/items.html +++ b/internal/api/templates/items.html @@ -447,6 +447,42 @@ placeholder="Item description" /> +
+ + +
+
+ + +
+
+ + +
+
+ + +
@@ -514,6 +550,42 @@ placeholder="Item description" />
+
+ + +
+
+ + +
+
+ + +
+
+ + +