package services import ( "net/http" "strconv" "time" "wm-backend/global" "wm-backend/internal/models" "wm-backend/internal/models/requests" "wm-backend/internal/models/responses" "wm-backend/internal/repositories" "wm-backend/pkg/helper" "wm-backend/response" db "wm-backend/sqlc_gen" "github.com/gin-gonic/gin" "github.com/rs/zerolog/log" ) // ComponentItemCreate creates a new component item. // It validates the request body and creates the component item in the database. // // @Summary Create a new component item // @Description Create a new component item with the provided details // @Tags component-item // @Accept json // @Produce json // @Param body body requests.CreateComponentItemRequest true "Component item request body" // @Success 201 {object} response.SuccessResponse{data=responses.CreateComponentItemResponse} // @Failure 400 {object} response.ErrorResponse // @Failure 500 {object} response.ErrorResponse // @Router /api/v1/component-items [post] func ComponentItemCreate(c *gin.Context) error { requestBody := requests.CreateComponentItemRequest{} if helper.IsShouldBindJSON(c, &requestBody) { return nil } componentItemModel := &models.ComponentItem{ ComponentID: requestBody.ComponentID, ContainerID: requestBody.ContainerID, Quantity: requestBody.Quantity, Status: requestBody.Status, Metadata: requestBody.Metadata, CreatedAt: time.Now(), } componentItem, err := repositories.CreateComponentItem(c.Request.Context(), global.Queries, *componentItemModel) if err != nil { response.InternalServerError(c, http.StatusInternalServerError, "Failed to create component item") return nil } response.Created(c, "Component item created successfully", &responses.CreateComponentItemResponse{ ID: componentItem.ID, }) return nil } // ComponentItemGetByID retrieves a single component item by its ID. // // @Summary Get component item by ID // @Description Retrieve a single component item using its unique identifier // @Tags component-item // @Accept json // @Produce json // @Param id path int true "Component item ID" // @Success 200 {object} response.SuccessResponse{data=models.ComponentItem} // @Failure 400 {object} response.ErrorResponse // @Failure 404 {object} response.ErrorResponse // @Failure 500 {object} response.ErrorResponse // @Router /api/v1/component-items/{id} [get] func ComponentItemGetByID(c *gin.Context) error { id, err := strconv.ParseInt(c.Param("id"), 10, 64) if err != nil { response.BadRequestError(c, http.StatusBadRequest, "Invalid ID") return nil } componentItem, err := repositories.GetComponentItemByID(c.Request.Context(), global.Queries, id) if err != nil { response.NotFoundError(c, http.StatusNotFound, "Component item not found") return nil } response.Ok(c, "Success", componentItem) return nil } // ComponentItemList retrieves all component items. // // @Summary List all component items // @Description Retrieve a list of all component items ordered by creation date // @Tags component-item // @Accept json // @Produce json // @Success 200 {object} response.SuccessResponse{data=[]models.ComponentItem} // @Failure 500 {object} response.ErrorResponse // @Router /api/v1/component-items [get] func ComponentItemList(c *gin.Context) error { componentItems, err := repositories.ListComponentItems(c.Request.Context(), global.Queries) if err != nil { response.InternalServerError(c, http.StatusInternalServerError, "Failed to list component items") return nil } response.Ok(c, "Success", componentItems) return nil } // ComponentItemUpdate updates an existing component item by its ID. // It validates the request body, fetches the existing record, // merges non-empty fields from the request, and updates the component item in the database. // // @Summary Update component item // @Description Update an existing component item by its ID. Only non-empty fields will be updated. // @Tags component-item // @Accept json // @Produce json // @Param id path int true "Component item ID" // @Param body body requests.UpdateComponentItemRequest true "Component item request body" // @Success 200 {object} response.SuccessResponse{data=responses.UpdateComponentItemResponse} // @Failure 400 {object} response.ErrorResponse // @Failure 404 {object} response.ErrorResponse // @Failure 500 {object} response.ErrorResponse // @Router /api/v1/component-items/{id} [put] func ComponentItemUpdate(c *gin.Context) error { id, err := strconv.ParseInt(c.Param("id"), 10, 64) if err != nil { response.BadRequestError(c, http.StatusBadRequest, "Invalid ID") return nil } requestBody := requests.UpdateComponentItemRequest{} if helper.IsShouldBindJSON(c, &requestBody) { return nil } existing, err := repositories.GetComponentItemByID(c.Request.Context(), global.Queries, id) if err != nil { response.NotFoundError(c, http.StatusNotFound, "Component item not found") return nil } if requestBody.ComponentID != nil { existing.ComponentID = *requestBody.ComponentID } if requestBody.ContainerID != nil { existing.ContainerID = *requestBody.ContainerID } if len(requestBody.Metadata) > 0 { existing.Metadata = requestBody.Metadata } existing.UpdatedAt = time.Now() componentItem, err := repositories.UpdateComponentItem(c.Request.Context(), global.Queries, existing) if err != nil { log.Error().Err(err).Msgf("Failed to update component item with ID: %d", id) response.InternalServerError(c, http.StatusInternalServerError, "Failed to update component item") return nil } response.Ok(c, "Component item updated successfully", &responses.UpdateComponentItemResponse{ ID: componentItem.ID, ComponentID: componentItem.ComponentID, ContainerID: componentItem.ContainerID, Quantity: componentItem.Quantity, Status: componentItem.Status, Metadata: componentItem.Metadata, }) return nil } // ComponentItemDelete deletes a component item by its ID. // // @Summary Delete component item // @Description Delete a component item by its unique identifier // @Tags component-item // @Accept json // @Produce json // @Param id path int true "Component item ID" // @Success 200 {object} response.SuccessResponse // @Failure 400 {object} response.ErrorResponse // @Failure 500 {object} response.ErrorResponse // @Router /api/v1/component-items/{id} [delete] func ComponentItemDelete(c *gin.Context) error { id, err := strconv.ParseInt(c.Param("id"), 10, 64) if err != nil { response.BadRequestError(c, http.StatusBadRequest, "Invalid ID") return nil } rowsAffected, err := repositories.DeleteComponentItem(c.Request.Context(), global.Queries, id) if err != nil { log.Error().Err(err).Msgf("Failed to delete component item with ID: %d", id) response.InternalServerError(c, http.StatusInternalServerError, "Failed to delete component item") return nil } if rowsAffected == 0 { response.NotFoundError(c, http.StatusNotFound, "Component item not found") return nil } response.Ok(c, "Delete Success", nil) return nil } // ComponentItemFind retrieves component items by component ID with full location info. // // @Summary Find component items by component ID // @Description Retrieve component items with full location details (container, shelf, cabinet, room, warehouse) for a given component ID // @Tags component-item // @Accept json // @Produce json // @Param componentId path int true "Component ID" // @Success 200 {object} response.SuccessResponse{data=[]models.FindComponentItemResult} // @Failure 400 {object} response.ErrorResponse // @Failure 500 {object} response.ErrorResponse // @Router /api/v1/component-items/find/{componentId} [get] func ComponentItemFind(c *gin.Context) error { componentID, err := strconv.ParseInt(c.Param("componentId"), 10, 64) if err != nil { response.BadRequestError(c, http.StatusBadRequest, "Invalid component ID") return nil } items, err := repositories.FindComponentItems(c.Request.Context(), global.Queries, componentID) if err != nil { log.Error().Err(err).Msgf("Failed to find component items for component ID: %d", componentID) response.InternalServerError(c, http.StatusInternalServerError, "Failed to find component items") return nil } response.Ok(c, "Success", items) return nil } // ComponentItemUpdateStatus changes the status of a component item. // It handles three cases within a single transaction: // - Case 1 (change all): changedQuantity is nil or equals quantity → updates status of entire record // - Case 2 (split): changedQuantity < quantity, no existing record with target status → reduces old record, creates new record // - Case 3 (merge): changedQuantity < quantity, existing record with target status → reduces old record, merges into existing // // A history record is always created. If quantity becomes 0 after the split, the old record is deleted. // // @Summary Change component item status // @Description Change the status of a component item. Supports partial quantity change with automatic split/merge logic. A status history record is created automatically. // @Tags component-item // @Accept json // @Produce json // @Param id path int true "Component item ID" // @Param body body requests.UpdateComponentItemStatusRequest true "Status change request body" // @Success 200 {object} response.SuccessResponse{data=responses.UpdateComponentItemStatusResponse} // @Failure 400 {object} response.ErrorResponse "Validation error (e.g., changed_quantity > quantity, status unchanged)" // @Failure 404 {object} response.ErrorResponse "Component item not found" // @Failure 500 {object} response.ErrorResponse "Internal server error" // @Router /api/v1/component-items/{id}/status [put] func ComponentItemUpdateStatus(c *gin.Context) error { id, err := strconv.ParseInt(c.Param("id"), 10, 64) if err != nil { response.BadRequestError(c, http.StatusBadRequest, "Invalid ID") return nil } var req requests.UpdateComponentItemStatusRequest if helper.IsShouldBindJSON(c, &req) { return nil } result, err := repositories.UpdateComponentItemStatus( c.Request.Context(), global.DB, id, db.ComponentItemStatusEnum(req.Status), req.ChangedQuantity, req.Note, "system", ) if err != nil { if err.Error() == "status unchanged" { response.Ok(c, "Status unchanged", nil) return nil } log.Error().Err(err).Msgf("Failed to update component item status for ID: %d", id) response.BadRequestError(c, http.StatusBadRequest, err.Error()) return nil } // Build response based on case resp := responses.UpdateComponentItemStatusResponse{ ID: id, OldStatus: result.StatusHistory.OldStatus, NewStatus: result.StatusHistory.NewStatus, ChangedQuantity: result.StatusHistory.ChangedQuantity, HistoryID: result.StatusHistory.ID, } if result.NewComponentItemID != nil { resp.NewComponentItemID = result.NewComponentItemID } if result.MergedComponentItemID != nil { resp.MergedComponentItemID = result.MergedComponentItemID } response.Ok(c, "Status updated successfully", resp) return nil }