feat: product proto type.
This commit is contained in:
@@ -21,6 +21,7 @@ type ProductInventoryAdminController struct {
|
||||
type productInventoryPayload struct {
|
||||
ProductCode string `json:"productCode"`
|
||||
ProjectTypeCode string `json:"projectTypeCode"`
|
||||
SuiteID *uint `json:"suiteId"`
|
||||
SuiteCode string `json:"suiteCode"`
|
||||
SerialNumber *string `json:"serialNumber"`
|
||||
AssetName string `json:"assetName"`
|
||||
@@ -88,11 +89,17 @@ func (pc *ProductInventoryAdminController) Create(c *gin.Context) {
|
||||
writeError(c, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
resolvedSuiteID, resolvedSuiteCode, err := pc.resolveSuite(payload.SuiteID, payload.SuiteCode)
|
||||
if err != nil {
|
||||
writeError(c, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
record := models.ProductInventory{
|
||||
ProductCode: payload.ProductCode,
|
||||
ProjectTypeCode: payload.ProjectTypeCode,
|
||||
SuiteCode: payload.SuiteCode,
|
||||
SuiteID: resolvedSuiteID,
|
||||
SuiteCode: resolvedSuiteCode,
|
||||
SerialNumber: payload.SerialNumber,
|
||||
AssetName: payload.AssetName,
|
||||
Status: models.ProductInventoryStatus(payload.Status),
|
||||
@@ -130,10 +137,16 @@ func (pc *ProductInventoryAdminController) Update(c *gin.Context) {
|
||||
writeError(c, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
resolvedSuiteID, resolvedSuiteCode, err := pc.resolveSuite(payload.SuiteID, payload.SuiteCode)
|
||||
if err != nil {
|
||||
writeError(c, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
record.ProductCode = payload.ProductCode
|
||||
record.ProjectTypeCode = payload.ProjectTypeCode
|
||||
record.SuiteCode = payload.SuiteCode
|
||||
record.SuiteID = resolvedSuiteID
|
||||
record.SuiteCode = resolvedSuiteCode
|
||||
record.SerialNumber = payload.SerialNumber
|
||||
record.AssetName = payload.AssetName
|
||||
record.Status = models.ProductInventoryStatus(payload.Status)
|
||||
@@ -219,6 +232,32 @@ func (pc *ProductInventoryAdminController) validateInventoryPayload(payload prod
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pc *ProductInventoryAdminController) resolveSuite(suiteID *uint, suiteCode string) (*uint, string, error) {
|
||||
if suiteID != nil && *suiteID > 0 {
|
||||
var suite models.ProductSuite
|
||||
if err := pc.DB.First(&suite, *suiteID).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, "", errors.New("suiteId is invalid")
|
||||
}
|
||||
return nil, "", errors.New("failed to query product suite")
|
||||
}
|
||||
return &suite.ID, suite.Code, nil
|
||||
}
|
||||
|
||||
if trimmedCode := strings.TrimSpace(suiteCode); trimmedCode != "" {
|
||||
var suite models.ProductSuite
|
||||
if err := pc.DB.Where("code = ?", trimmedCode).First(&suite).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, "", errors.New("suiteCode is invalid")
|
||||
}
|
||||
return nil, "", errors.New("failed to query product suite")
|
||||
}
|
||||
return &suite.ID, suite.Code, nil
|
||||
}
|
||||
|
||||
return nil, "", nil
|
||||
}
|
||||
|
||||
func (pc *ProductInventoryAdminController) applySoldDefaults(record *models.ProductInventory) {
|
||||
if record == nil {
|
||||
return
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"hr_receiver/config"
|
||||
"hr_receiver/models"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ProductPrototypeAdminController struct {
|
||||
DB *gorm.DB
|
||||
}
|
||||
|
||||
type productPrototypePayload struct {
|
||||
Name string `json:"name"`
|
||||
ProductCode string `json:"productCode"`
|
||||
ProjectTypeCode string `json:"projectTypeCode"`
|
||||
AssetName string `json:"assetName"`
|
||||
Status string `json:"status"`
|
||||
RegionID *uint32 `json:"regionId"`
|
||||
StorageLocation string `json:"storageLocation"`
|
||||
ParameterValues string `json:"parameterValues"`
|
||||
Notes string `json:"notes"`
|
||||
}
|
||||
|
||||
func NewProductPrototypeAdminController() *ProductPrototypeAdminController {
|
||||
return &ProductPrototypeAdminController{DB: config.DB}
|
||||
}
|
||||
|
||||
func (pc *ProductPrototypeAdminController) List(c *gin.Context) {
|
||||
var items []models.ProductPrototype
|
||||
query := pc.DB.Model(&models.ProductPrototype{}).Order("updated_at DESC, id DESC")
|
||||
|
||||
if keyword := strings.TrimSpace(c.Query("keyword")); keyword != "" {
|
||||
likeValue := "%" + keyword + "%"
|
||||
query = query.Where("name LIKE ? OR notes LIKE ?", likeValue, likeValue)
|
||||
}
|
||||
if productCode := strings.TrimSpace(c.Query("productCode")); productCode != "" {
|
||||
query = query.Where("product_code = ?", strings.ToLower(productCode))
|
||||
}
|
||||
if projectTypeCode := strings.TrimSpace(c.Query("projectTypeCode")); projectTypeCode != "" {
|
||||
query = query.Where("project_type_code = ?", strings.ToLower(projectTypeCode))
|
||||
}
|
||||
|
||||
if err := query.Find(&items).Error; err != nil {
|
||||
writeError(c, http.StatusInternalServerError, "failed to query product prototypes")
|
||||
return
|
||||
}
|
||||
writeSuccess(c, http.StatusOK, "query success", items)
|
||||
}
|
||||
|
||||
func (pc *ProductPrototypeAdminController) Create(c *gin.Context) {
|
||||
var payload productPrototypePayload
|
||||
if err := c.ShouldBindJSON(&payload); err != nil {
|
||||
writeError(c, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
if err := pc.validatePayload(payload); err != nil {
|
||||
writeError(c, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
record := models.ProductPrototype{
|
||||
Name: payload.Name,
|
||||
ProductCode: payload.ProductCode,
|
||||
ProjectTypeCode: payload.ProjectTypeCode,
|
||||
AssetName: payload.AssetName,
|
||||
Status: payload.Status,
|
||||
RegionID: payload.RegionID,
|
||||
StorageLocation: payload.StorageLocation,
|
||||
ParameterValues: payload.ParameterValues,
|
||||
Notes: payload.Notes,
|
||||
}
|
||||
if err := pc.DB.Create(&record).Error; err != nil {
|
||||
if strings.Contains(strings.ToLower(err.Error()), "unique") {
|
||||
writeError(c, http.StatusConflict, "prototype already exists")
|
||||
return
|
||||
}
|
||||
writeError(c, http.StatusInternalServerError, "failed to create prototype")
|
||||
return
|
||||
}
|
||||
writeSuccess(c, http.StatusCreated, "create success", record)
|
||||
}
|
||||
|
||||
func (pc *ProductPrototypeAdminController) Update(c *gin.Context) {
|
||||
record, err := pc.findByID(c.Param("id"))
|
||||
if err != nil {
|
||||
respondProductPrototypeLookupError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
var payload productPrototypePayload
|
||||
if err := c.ShouldBindJSON(&payload); err != nil {
|
||||
writeError(c, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
if err := pc.validatePayload(payload); err != nil {
|
||||
writeError(c, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
record.Name = payload.Name
|
||||
record.ProductCode = payload.ProductCode
|
||||
record.ProjectTypeCode = payload.ProjectTypeCode
|
||||
record.AssetName = payload.AssetName
|
||||
record.Status = payload.Status
|
||||
record.RegionID = payload.RegionID
|
||||
record.StorageLocation = payload.StorageLocation
|
||||
record.ParameterValues = payload.ParameterValues
|
||||
record.Notes = payload.Notes
|
||||
|
||||
if err := pc.DB.Save(&record).Error; err != nil {
|
||||
writeError(c, http.StatusInternalServerError, "failed to update prototype")
|
||||
return
|
||||
}
|
||||
writeSuccess(c, http.StatusOK, "update success", record)
|
||||
}
|
||||
|
||||
func (pc *ProductPrototypeAdminController) Delete(c *gin.Context) {
|
||||
record, err := pc.findByID(c.Param("id"))
|
||||
if err != nil {
|
||||
respondProductPrototypeLookupError(c, err)
|
||||
return
|
||||
}
|
||||
if err := pc.DB.Delete(&record).Error; err != nil {
|
||||
writeError(c, http.StatusInternalServerError, "failed to delete prototype")
|
||||
return
|
||||
}
|
||||
writeSuccess(c, http.StatusOK, "delete success", nil)
|
||||
}
|
||||
|
||||
func (pc *ProductPrototypeAdminController) findByID(id string) (models.ProductPrototype, error) {
|
||||
var record models.ProductPrototype
|
||||
numericID, err := strconv.ParseUint(strings.TrimSpace(id), 10, 64)
|
||||
if err != nil {
|
||||
return record, gorm.ErrRecordNotFound
|
||||
}
|
||||
if err := pc.DB.First(&record, numericID).Error; err != nil {
|
||||
return record, err
|
||||
}
|
||||
return record, nil
|
||||
}
|
||||
|
||||
func (pc *ProductPrototypeAdminController) validatePayload(payload productPrototypePayload) error {
|
||||
if strings.TrimSpace(payload.Name) == "" {
|
||||
return errors.New("name is required")
|
||||
}
|
||||
productCode := strings.TrimSpace(strings.ToLower(payload.ProductCode))
|
||||
if productCode == "" {
|
||||
return errors.New("productCode is required")
|
||||
}
|
||||
var product models.ProductDefinition
|
||||
if err := pc.DB.Where("code = ? AND is_active = ?", productCode, true).First(&product).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return errors.New("productCode is invalid")
|
||||
}
|
||||
return errors.New("failed to query product definition")
|
||||
}
|
||||
if strings.TrimSpace(payload.ProjectTypeCode) != "" {
|
||||
var projectType models.ProjectType
|
||||
if err := pc.DB.Where("code = ? AND is_active = ?", strings.ToLower(strings.TrimSpace(payload.ProjectTypeCode)), true).First(&projectType).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return errors.New("projectTypeCode is invalid")
|
||||
}
|
||||
return errors.New("failed to query project type")
|
||||
}
|
||||
}
|
||||
switch models.ProductInventoryStatus(strings.ToLower(strings.TrimSpace(payload.Status))) {
|
||||
case "", models.ProductInventoryStatusInStock, models.ProductInventoryStatusSold, models.ProductInventoryStatusMaintenance, models.ProductInventoryStatusRetired:
|
||||
default:
|
||||
return errors.New("status is invalid")
|
||||
}
|
||||
if strings.TrimSpace(payload.ParameterValues) != "" && !json.Valid([]byte(strings.TrimSpace(payload.ParameterValues))) {
|
||||
return errors.New("parameterValues is invalid")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func respondProductPrototypeLookupError(c *gin.Context, err error) {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
writeError(c, http.StatusNotFound, "product prototype not found")
|
||||
return
|
||||
}
|
||||
writeError(c, http.StatusInternalServerError, "failed to query product prototype")
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"hr_receiver/config"
|
||||
"hr_receiver/models"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ProductSuiteAdminController struct {
|
||||
DB *gorm.DB
|
||||
}
|
||||
|
||||
type productSuitePayload struct {
|
||||
Code string `json:"code"`
|
||||
Name string `json:"name"`
|
||||
ProjectTypeCode string `json:"projectTypeCode"`
|
||||
RegionID *uint32 `json:"regionId"`
|
||||
SoldTargetType string `json:"soldTargetType"`
|
||||
SoldTargetName string `json:"soldTargetName"`
|
||||
Notes string `json:"notes"`
|
||||
InventoryIDs []uint `json:"inventoryIds"`
|
||||
}
|
||||
|
||||
type productSuiteListItem struct {
|
||||
models.ProductSuite
|
||||
Inventories []models.ProductInventory `json:"inventories"`
|
||||
}
|
||||
|
||||
func NewProductSuiteAdminController() *ProductSuiteAdminController {
|
||||
return &ProductSuiteAdminController{DB: config.DB}
|
||||
}
|
||||
|
||||
func (pc *ProductSuiteAdminController) List(c *gin.Context) {
|
||||
var suites []models.ProductSuite
|
||||
query := pc.DB.Model(&models.ProductSuite{}).Order("updated_at DESC, id DESC")
|
||||
|
||||
if keyword := strings.TrimSpace(c.Query("keyword")); keyword != "" {
|
||||
likeValue := "%" + keyword + "%"
|
||||
query = query.Where("code LIKE ? OR name LIKE ? OR notes LIKE ?", likeValue, likeValue, likeValue)
|
||||
}
|
||||
if projectTypeCode := strings.TrimSpace(c.Query("projectTypeCode")); projectTypeCode != "" {
|
||||
query = query.Where("project_type_code = ?", strings.ToLower(projectTypeCode))
|
||||
}
|
||||
|
||||
if err := query.Find(&suites).Error; err != nil {
|
||||
writeError(c, http.StatusInternalServerError, "failed to query product suites")
|
||||
return
|
||||
}
|
||||
|
||||
result := make([]productSuiteListItem, 0, len(suites))
|
||||
for _, suite := range suites {
|
||||
var inventories []models.ProductInventory
|
||||
if err := pc.DB.Where("suite_id = ?", suite.ID).Order("updated_at DESC, id DESC").Find(&inventories).Error; err != nil {
|
||||
writeError(c, http.StatusInternalServerError, "failed to query suite inventories")
|
||||
return
|
||||
}
|
||||
result = append(result, productSuiteListItem{
|
||||
ProductSuite: suite,
|
||||
Inventories: inventories,
|
||||
})
|
||||
}
|
||||
|
||||
writeSuccess(c, http.StatusOK, "query success", result)
|
||||
}
|
||||
|
||||
func (pc *ProductSuiteAdminController) Create(c *gin.Context) {
|
||||
var payload productSuitePayload
|
||||
if err := c.ShouldBindJSON(&payload); err != nil {
|
||||
writeError(c, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
if err := pc.validatePayload(payload); err != nil {
|
||||
writeError(c, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
record := models.ProductSuite{
|
||||
Code: payload.Code,
|
||||
Name: payload.Name,
|
||||
ProjectTypeCode: payload.ProjectTypeCode,
|
||||
RegionID: payload.RegionID,
|
||||
SoldTargetType: payload.SoldTargetType,
|
||||
SoldTargetName: payload.SoldTargetName,
|
||||
Notes: payload.Notes,
|
||||
}
|
||||
|
||||
if err := pc.DB.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Create(&record).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return pc.syncSuiteInventories(tx, record, payload.InventoryIDs)
|
||||
}); err != nil {
|
||||
if strings.Contains(strings.ToLower(err.Error()), "unique") {
|
||||
writeError(c, http.StatusConflict, "suite code already exists")
|
||||
return
|
||||
}
|
||||
writeError(c, http.StatusInternalServerError, "failed to create suite")
|
||||
return
|
||||
}
|
||||
|
||||
writeSuccess(c, http.StatusCreated, "create success", record)
|
||||
}
|
||||
|
||||
func (pc *ProductSuiteAdminController) Update(c *gin.Context) {
|
||||
record, err := pc.findByID(c.Param("id"))
|
||||
if err != nil {
|
||||
respondProductSuiteLookupError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
var payload productSuitePayload
|
||||
if err := c.ShouldBindJSON(&payload); err != nil {
|
||||
writeError(c, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
if err := pc.validatePayload(payload); err != nil {
|
||||
writeError(c, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
record.Code = payload.Code
|
||||
record.Name = payload.Name
|
||||
record.ProjectTypeCode = payload.ProjectTypeCode
|
||||
record.RegionID = payload.RegionID
|
||||
record.SoldTargetType = payload.SoldTargetType
|
||||
record.SoldTargetName = payload.SoldTargetName
|
||||
record.Notes = payload.Notes
|
||||
|
||||
if err := pc.DB.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Save(&record).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return pc.syncSuiteInventories(tx, record, payload.InventoryIDs)
|
||||
}); err != nil {
|
||||
if strings.Contains(strings.ToLower(err.Error()), "unique") {
|
||||
writeError(c, http.StatusConflict, "suite code already exists")
|
||||
return
|
||||
}
|
||||
writeError(c, http.StatusInternalServerError, "failed to update suite")
|
||||
return
|
||||
}
|
||||
|
||||
writeSuccess(c, http.StatusOK, "update success", record)
|
||||
}
|
||||
|
||||
func (pc *ProductSuiteAdminController) Delete(c *gin.Context) {
|
||||
record, err := pc.findByID(c.Param("id"))
|
||||
if err != nil {
|
||||
respondProductSuiteLookupError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := pc.DB.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Model(&models.ProductInventory{}).Where("suite_id = ?", record.ID).Updates(map[string]interface{}{
|
||||
"suite_id": nil,
|
||||
"suite_code": "",
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Delete(&record).Error
|
||||
}); err != nil {
|
||||
writeError(c, http.StatusInternalServerError, "failed to delete suite")
|
||||
return
|
||||
}
|
||||
|
||||
writeSuccess(c, http.StatusOK, "delete success", nil)
|
||||
}
|
||||
|
||||
func (pc *ProductSuiteAdminController) findByID(id string) (models.ProductSuite, error) {
|
||||
var record models.ProductSuite
|
||||
numericID, err := strconv.ParseUint(strings.TrimSpace(id), 10, 64)
|
||||
if err != nil {
|
||||
return record, gorm.ErrRecordNotFound
|
||||
}
|
||||
if err := pc.DB.First(&record, numericID).Error; err != nil {
|
||||
return record, err
|
||||
}
|
||||
return record, nil
|
||||
}
|
||||
|
||||
func (pc *ProductSuiteAdminController) validatePayload(payload productSuitePayload) error {
|
||||
if strings.TrimSpace(payload.Code) == "" {
|
||||
return errors.New("code is required")
|
||||
}
|
||||
if strings.TrimSpace(payload.Name) == "" {
|
||||
return errors.New("name is required")
|
||||
}
|
||||
if strings.TrimSpace(payload.ProjectTypeCode) != "" {
|
||||
var projectType models.ProjectType
|
||||
if err := pc.DB.Where("code = ? AND is_active = ?", strings.ToLower(strings.TrimSpace(payload.ProjectTypeCode)), true).First(&projectType).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return errors.New("projectTypeCode is invalid")
|
||||
}
|
||||
return errors.New("failed to query project type")
|
||||
}
|
||||
}
|
||||
if strings.TrimSpace(payload.SoldTargetType) != "" {
|
||||
switch strings.ToLower(strings.TrimSpace(payload.SoldTargetType)) {
|
||||
case "kindergarten", "school", "dealer", "person", "other":
|
||||
default:
|
||||
return errors.New("soldTargetType is invalid")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pc *ProductSuiteAdminController) syncSuiteInventories(tx *gorm.DB, suite models.ProductSuite, inventoryIDs []uint) error {
|
||||
if err := tx.Model(&models.ProductInventory{}).Where("suite_id = ?", suite.ID).Updates(map[string]interface{}{
|
||||
"suite_id": nil,
|
||||
"suite_code": "",
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if len(inventoryIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var count int64
|
||||
if err := tx.Model(&models.ProductInventory{}).Where("id IN ?", inventoryIDs).Count(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if count != int64(len(inventoryIDs)) {
|
||||
return errors.New("some inventory records do not exist")
|
||||
}
|
||||
|
||||
return tx.Model(&models.ProductInventory{}).Where("id IN ?", inventoryIDs).Updates(map[string]interface{}{
|
||||
"suite_id": suite.ID,
|
||||
"suite_code": suite.Code,
|
||||
}).Error
|
||||
}
|
||||
|
||||
func respondProductSuiteLookupError(c *gin.Context, err error) {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
writeError(c, http.StatusNotFound, "product suite not found")
|
||||
return
|
||||
}
|
||||
writeError(c, http.StatusInternalServerError, "failed to query product suite")
|
||||
}
|
||||
Reference in New Issue
Block a user