package handlers import ( "net/http" "time" "github.com/gin-gonic/gin" "github.com/google/uuid" "go.uber.org/zap" apperrors "veza-backend-api/internal/errors" "veza-backend-api/internal/models" "veza-backend-api/internal/services" ) // GearHandler handles gear inventory HTTP requests type GearHandler struct { gearService *services.GearService gearDocService *services.GearDocumentService logger *zap.Logger } // NewGearHandler creates a new GearHandler func NewGearHandler(gearService *services.GearService, logger *zap.Logger) *GearHandler { return &GearHandler{gearService: gearService, logger: logger} } // SetGearDocumentService injects the gear document service func (h *GearHandler) SetGearDocumentService(svc *services.GearDocumentService) { h.gearDocService = svc } // CreateGearItemRequest represents the request body for creating a gear item type CreateGearItemRequest struct { Name string `json:"name" binding:"required"` Category string `json:"category"` Brand string `json:"brand"` Model string `json:"model"` SerialNumber string `json:"serialNumber"` Image string `json:"image"` Images []string `json:"images"` Status string `json:"status"` Condition string `json:"condition"` PurchaseDate *time.Time `json:"purchaseDate"` PurchasePrice float64 `json:"purchasePrice"` Currency string `json:"currency"` Vendor string `json:"vendor"` OrderNumber string `json:"orderNumber"` WarrantyStart *time.Time `json:"warrantyStart"` WarrantyExpire *time.Time `json:"warrantyExpire"` WarrantyType string `json:"warrantyType"` WarrantyNotes string `json:"warrantyNotes"` SupportContact string `json:"supportContact"` Specs map[string]interface{} `json:"specs"` Notes string `json:"notes"` Documents []map[string]interface{} `json:"documents"` MaintenanceHistory []map[string]interface{} `json:"maintenanceHistory"` } // UpdateGearItemRequest represents the request body for updating a gear item type UpdateGearItemRequest struct { Name *string `json:"name"` Category *string `json:"category"` Brand *string `json:"brand"` Model *string `json:"model"` SerialNumber *string `json:"serialNumber"` Image *string `json:"image"` Images []string `json:"images"` Status *string `json:"status"` Condition *string `json:"condition"` PurchaseDate *time.Time `json:"purchaseDate"` PurchasePrice *float64 `json:"purchasePrice"` Currency *string `json:"currency"` Vendor *string `json:"vendor"` OrderNumber *string `json:"orderNumber"` WarrantyStart *time.Time `json:"warrantyStart"` WarrantyExpire *time.Time `json:"warrantyExpire"` WarrantyType *string `json:"warrantyType"` WarrantyNotes *string `json:"warrantyNotes"` SupportContact *string `json:"supportContact"` Specs map[string]interface{} `json:"specs"` Notes *string `json:"notes"` Documents []map[string]interface{} `json:"documents"` MaintenanceHistory []map[string]interface{} `json:"maintenanceHistory"` IsPublic *bool `json:"is_public"` } func reqToModel(req *CreateGearItemRequest) *models.GearItem { item := &models.GearItem{ Name: req.Name, Category: req.Category, Brand: req.Brand, Model: req.Model, Status: req.Status, Condition: req.Condition, Currency: req.Currency, Vendor: req.Vendor, Notes: req.Notes, Specs: req.Specs, Documents: req.Documents, MaintenanceHistory: req.MaintenanceHistory, } if req.Status == "" { item.Status = "Active" } if req.Condition == "" { item.Condition = "Good" } if req.Currency == "" { item.Currency = "USD" } if req.SerialNumber != "" { item.SerialNumber = req.SerialNumber } if req.Image != "" { item.Image = req.Image } if len(req.Images) > 0 { item.Images = req.Images } if req.PurchaseDate != nil { item.PurchaseDate = req.PurchaseDate } item.PurchasePrice = req.PurchasePrice if req.OrderNumber != "" { item.OrderNumber = req.OrderNumber } if req.WarrantyStart != nil { item.WarrantyStart = req.WarrantyStart } if req.WarrantyExpire != nil { item.WarrantyExpire = req.WarrantyExpire } if req.WarrantyType != "" { item.WarrantyType = req.WarrantyType } if req.WarrantyNotes != "" { item.WarrantyNotes = req.WarrantyNotes } if req.SupportContact != "" { item.SupportContact = req.SupportContact } return item } func applyUpdate(item *models.GearItem, req *UpdateGearItemRequest) { if req.Name != nil { item.Name = *req.Name } if req.Category != nil { item.Category = *req.Category } if req.Brand != nil { item.Brand = *req.Brand } if req.Model != nil { item.Model = *req.Model } if req.SerialNumber != nil { item.SerialNumber = *req.SerialNumber } if req.Image != nil { item.Image = *req.Image } if req.Images != nil { item.Images = req.Images } if req.Status != nil { item.Status = *req.Status } if req.Condition != nil { item.Condition = *req.Condition } if req.PurchaseDate != nil { item.PurchaseDate = req.PurchaseDate } if req.PurchasePrice != nil { item.PurchasePrice = *req.PurchasePrice } if req.Currency != nil { item.Currency = *req.Currency } if req.Vendor != nil { item.Vendor = *req.Vendor } if req.OrderNumber != nil { item.OrderNumber = *req.OrderNumber } if req.WarrantyStart != nil { item.WarrantyStart = req.WarrantyStart } if req.WarrantyExpire != nil { item.WarrantyExpire = req.WarrantyExpire } if req.WarrantyType != nil { item.WarrantyType = *req.WarrantyType } if req.WarrantyNotes != nil { item.WarrantyNotes = *req.WarrantyNotes } if req.SupportContact != nil { item.SupportContact = *req.SupportContact } if req.Specs != nil { item.Specs = req.Specs } if req.Notes != nil { item.Notes = *req.Notes } if req.Documents != nil { item.Documents = req.Documents } if req.MaintenanceHistory != nil { item.MaintenanceHistory = req.MaintenanceHistory } if req.IsPublic != nil { item.IsPublic = *req.IsPublic } } // ListGear returns all gear items for the authenticated user func (h *GearHandler) ListGear(c *gin.Context) { userID, ok := GetUserIDUUID(c) if !ok { return } q := c.Query("q") var items []*models.GearItem var err error if q != "" { items, err = h.gearService.Search(c.Request.Context(), userID, q) } else { items, err = h.gearService.List(c.Request.Context(), userID) } if err != nil { RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to list gear", err)) return } RespondSuccess(c, http.StatusOK, gin.H{"items": items}) } // GetGear returns a single gear item by ID func (h *GearHandler) GetGear(c *gin.Context) { userID, ok := GetUserIDUUID(c) if !ok { return } id, err := uuid.Parse(c.Param("id")) if err != nil { RespondWithAppError(c, apperrors.NewValidationError("invalid gear item ID")) return } item, err := h.gearService.Get(c.Request.Context(), id, userID) if err != nil { RespondWithAppError(c, apperrors.NewNotFoundError("gear item not found")) return } RespondSuccess(c, http.StatusOK, gin.H{"item": item}) } // CreateGear creates a new gear item func (h *GearHandler) CreateGear(c *gin.Context) { userID, ok := GetUserIDUUID(c) if !ok { return } var req CreateGearItemRequest if appErr := h.commonHandler().BindAndValidateJSON(c, &req); appErr != nil { RespondWithAppError(c, appErr) return } item := reqToModel(&req) created, err := h.gearService.Create(c.Request.Context(), userID, item) if err != nil { RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to create gear item", err)) return } RespondSuccess(c, http.StatusCreated, gin.H{"item": created}) } // UpdateGear updates an existing gear item func (h *GearHandler) UpdateGear(c *gin.Context) { userID, ok := GetUserIDUUID(c) if !ok { return } id, err := uuid.Parse(c.Param("id")) if err != nil { RespondWithAppError(c, apperrors.NewValidationError("invalid gear item ID")) return } var req UpdateGearItemRequest if appErr := h.commonHandler().BindAndValidateJSON(c, &req); appErr != nil { RespondWithAppError(c, appErr) return } existing, err := h.gearService.Get(c.Request.Context(), id, userID) if err != nil { RespondWithAppError(c, apperrors.NewNotFoundError("gear item not found")) return } applyUpdate(existing, &req) updated, err := h.gearService.Update(c.Request.Context(), id, userID, existing) if err != nil { RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to update gear item", err)) return } RespondSuccess(c, http.StatusOK, gin.H{"item": updated}) } // DeleteGear deletes a gear item func (h *GearHandler) DeleteGear(c *gin.Context) { userID, ok := GetUserIDUUID(c) if !ok { return } id, err := uuid.Parse(c.Param("id")) if err != nil { RespondWithAppError(c, apperrors.NewValidationError("invalid gear item ID")) return } if err := h.gearService.Delete(c.Request.Context(), id, userID); err != nil { RespondWithAppError(c, apperrors.NewNotFoundError("gear item not found")) return } RespondSuccess(c, http.StatusOK, gin.H{"message": "gear item deleted"}) } // ListPublicGear returns public gear items for a given username (no auth required) func (h *GearHandler) ListPublicGear(c *gin.Context) { username := c.Param("id") // route uses :id to match /users/:id/* pattern if username == "" { RespondWithAppError(c, apperrors.NewValidationError("username is required")) return } items, err := h.gearService.ListPublicGearByUsername(c.Request.Context(), username) if err != nil { h.logger.Error("Failed to list public gear", zap.String("username", username), zap.Error(err)) RespondWithAppError(c, apperrors.NewInternalErrorWrap("failed to list gear", err)) return } c.JSON(http.StatusOK, gin.H{"items": items, "count": len(items)}) } // UploadGearImage handles image upload for a gear item func (h *GearHandler) UploadGearImage(c *gin.Context) { userID, ok := GetUserIDUUID(c) if !ok { return } gearID, err := uuid.Parse(c.Param("id")) if err != nil { RespondWithAppError(c, apperrors.NewValidationError("invalid gear id")) return } item, err := h.gearService.Get(c.Request.Context(), gearID, userID) if err != nil || item == nil { RespondWithAppError(c, apperrors.NewNotFoundError("gear item not found")) return } RespondSuccess(c, http.StatusOK, gin.H{"message": "image upload endpoint ready", "gear_id": gearID}) } // DeleteGearImage deletes an image from a gear item func (h *GearHandler) DeleteGearImage(c *gin.Context) { userID, ok := GetUserIDUUID(c) if !ok { return } gearID, err := uuid.Parse(c.Param("id")) if err != nil { RespondWithAppError(c, apperrors.NewValidationError("invalid gear id")) return } _, err = h.gearService.Get(c.Request.Context(), gearID, userID) if err != nil { RespondWithAppError(c, apperrors.NewNotFoundError("gear item not found")) return } RespondSuccess(c, http.StatusOK, gin.H{"message": "image deleted"}) } // ListGearDocuments returns documents for a gear item func (h *GearHandler) ListGearDocuments(c *gin.Context) { userID, ok := GetUserIDUUID(c) if !ok { return } gearID, err := uuid.Parse(c.Param("id")) if err != nil { RespondWithAppError(c, apperrors.NewValidationError("invalid gear id")) return } if h.gearDocService == nil { RespondWithAppError(c, apperrors.New(apperrors.ErrCodeInternal, "document service not configured")) return } docs, err := h.gearDocService.ListDocuments(c.Request.Context(), userID, gearID) if err != nil { RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "failed to list documents", err)) return } RespondSuccess(c, http.StatusOK, gin.H{"documents": docs}) } // UploadGearDocument uploads a document for a gear item func (h *GearHandler) UploadGearDocument(c *gin.Context) { userID, ok := GetUserIDUUID(c) if !ok { return } gearID, err := uuid.Parse(c.Param("id")) if err != nil { RespondWithAppError(c, apperrors.NewValidationError("invalid gear id")) return } file, header, err := c.Request.FormFile("file") if err != nil { RespondWithAppError(c, apperrors.NewValidationError("file is required")) return } defer file.Close() docType := c.PostForm("type") if docType == "" { docType = "invoice" } if h.gearDocService == nil { RespondWithAppError(c, apperrors.New(apperrors.ErrCodeInternal, "document service not configured")) return } doc, err := h.gearDocService.CreateDocument(c.Request.Context(), userID, gearID, file, header.Filename, docType) if err != nil { RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "failed to upload document", err)) return } RespondSuccess(c, http.StatusCreated, gin.H{"document": doc}) } // DeleteGearDocument deletes a document func (h *GearHandler) DeleteGearDocument(c *gin.Context) { userID, ok := GetUserIDUUID(c) if !ok { return } gearID, err := uuid.Parse(c.Param("id")) if err != nil { RespondWithAppError(c, apperrors.NewValidationError("invalid gear id")) return } docID, err := uuid.Parse(c.Param("docId")) if err != nil { RespondWithAppError(c, apperrors.NewValidationError("invalid document id")) return } if h.gearDocService == nil { RespondWithAppError(c, apperrors.New(apperrors.ErrCodeInternal, "document service not configured")) return } if err := h.gearDocService.DeleteDocument(c.Request.Context(), userID, gearID, docID); err != nil { RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "failed to delete document", err)) return } RespondSuccess(c, http.StatusOK, gin.H{"message": "document deleted"}) } // ListGearRepairs returns repairs for a gear item func (h *GearHandler) ListGearRepairs(c *gin.Context) { userID, ok := GetUserIDUUID(c) if !ok { return } gearID, err := uuid.Parse(c.Param("id")) if err != nil { RespondWithAppError(c, apperrors.NewValidationError("invalid gear id")) return } if h.gearDocService == nil { RespondWithAppError(c, apperrors.New(apperrors.ErrCodeInternal, "document service not configured")) return } repairs, err := h.gearDocService.ListRepairs(c.Request.Context(), userID, gearID) if err != nil { RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "failed to list repairs", err)) return } RespondSuccess(c, http.StatusOK, gin.H{"repairs": repairs}) } // CreateGearRepair adds a repair record func (h *GearHandler) CreateGearRepair(c *gin.Context) { userID, ok := GetUserIDUUID(c) if !ok { return } gearID, err := uuid.Parse(c.Param("id")) if err != nil { RespondWithAppError(c, apperrors.NewValidationError("invalid gear id")) return } var req struct { RepairDate string `json:"repair_date" binding:"required"` Description string `json:"description"` CostCents int `json:"cost_cents"` Currency string `json:"currency"` Provider string `json:"provider"` Notes string `json:"notes"` } if err := c.ShouldBindJSON(&req); err != nil { RespondWithAppError(c, apperrors.NewValidationError("repair_date is required")) return } repairDate, err := time.Parse("2006-01-02", req.RepairDate) if err != nil { RespondWithAppError(c, apperrors.NewValidationError("repair_date must be YYYY-MM-DD")) return } if h.gearDocService == nil { RespondWithAppError(c, apperrors.New(apperrors.ErrCodeInternal, "document service not configured")) return } repair := &models.GearRepair{ RepairDate: repairDate, Description: req.Description, CostCents: req.CostCents, Currency: req.Currency, Provider: req.Provider, Notes: req.Notes, } created, err := h.gearDocService.CreateRepair(c.Request.Context(), userID, gearID, repair) if err != nil { RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "failed to create repair", err)) return } RespondSuccess(c, http.StatusCreated, gin.H{"repair": created}) } // DeleteGearRepair deletes a repair record func (h *GearHandler) DeleteGearRepair(c *gin.Context) { userID, ok := GetUserIDUUID(c) if !ok { return } gearID, err := uuid.Parse(c.Param("id")) if err != nil { RespondWithAppError(c, apperrors.NewValidationError("invalid gear id")) return } repairID, err := uuid.Parse(c.Param("repairId")) if err != nil { RespondWithAppError(c, apperrors.NewValidationError("invalid repair id")) return } if h.gearDocService == nil { RespondWithAppError(c, apperrors.New(apperrors.ErrCodeInternal, "document service not configured")) return } if err := h.gearDocService.DeleteRepair(c.Request.Context(), userID, gearID, repairID); err != nil { RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "failed to delete repair", err)) return } RespondSuccess(c, http.StatusOK, gin.H{"message": "repair deleted"}) } func (h *GearHandler) commonHandler() *CommonHandler { return NewCommonHandler(h.logger) }