574 lines
18 KiB
Go
574 lines
18 KiB
Go
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)
|
|
}
|