diff --git a/internal/database/migrations/20260126075643_remove_stock_log_id_from_adjustment_stocks.down.sql b/internal/database/migrations/20260126075643_remove_stock_log_id_from_adjustment_stocks.down.sql new file mode 100644 index 00000000..e8f203a8 --- /dev/null +++ b/internal/database/migrations/20260126075643_remove_stock_log_id_from_adjustment_stocks.down.sql @@ -0,0 +1,3 @@ +ALTER TABLE adjustment_stocks ADD COLUMN stock_log_id INTEGER; + +CREATE INDEX idx_adjustment_stocks_stock_log_id ON adjustment_stocks (stock_log_id); \ No newline at end of file diff --git a/internal/database/migrations/20260126075643_remove_stock_log_id_from_adjustment_stocks.up.sql b/internal/database/migrations/20260126075643_remove_stock_log_id_from_adjustment_stocks.up.sql new file mode 100644 index 00000000..32f27161 --- /dev/null +++ b/internal/database/migrations/20260126075643_remove_stock_log_id_from_adjustment_stocks.up.sql @@ -0,0 +1 @@ +ALTER TABLE adjustment_stocks DROP COLUMN IF EXISTS stock_log_id; \ No newline at end of file diff --git a/internal/entities/adjustment_stock.go b/internal/entities/adjustment_stock.go index ef27d0c2..841e4820 100644 --- a/internal/entities/adjustment_stock.go +++ b/internal/entities/adjustment_stock.go @@ -4,7 +4,6 @@ import "time" type AdjustmentStock struct { Id uint `gorm:"primaryKey"` - StockLogId uint `gorm:"column:stock_log_id;not null;index"` ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null"` TotalQty float64 `gorm:"column:total_qty;default:0"` TotalUsed float64 `gorm:"column:total_used;default:0"` @@ -13,6 +12,6 @@ type AdjustmentStock struct { CreatedAt time.Time `gorm:"column:created_at;autoCreateTime"` UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime"` - StockLog *StockLog `gorm:"foreignKey:StockLogId;references:Id"` ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"` + StockLog *StockLog `gorm:"polymorphic:Loggable;polymorphicType:LoggableType;polymorphicId:LoggableId;polymorphicValue:ADJUSTMENT"` } diff --git a/internal/modules/closings/dto/closingMarketing.dto.go b/internal/modules/closings/dto/closingMarketing.dto.go index 72523b69..a4bb5cb0 100644 --- a/internal/modules/closings/dto/closingMarketing.dto.go +++ b/internal/modules/closings/dto/closingMarketing.dto.go @@ -8,6 +8,7 @@ import ( customerDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/dto" kandangDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto" productDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/dto" + "gitlab.com/mbugroup/lti-api.git/internal/utils" ) // === Response DTO === @@ -49,7 +50,12 @@ func ToSalesDTO(e entity.MarketingDeliveryProduct) SalesDTO { productFlags[i] = f.Name } - ageInDay, ageInWeeks := calculateAgeFromChickin(e.MarketingProduct.ProductWarehouse.ProjectFlockKandang, e.DeliveryDate, productFlags) + var category string + if e.MarketingProduct.ProductWarehouse.ProjectFlockKandang != nil { + category = e.MarketingProduct.ProductWarehouse.ProjectFlockKandang.ProjectFlock.Category + } + + ageInDay, ageInWeeks := calculateAgeFromChickin(e.MarketingProduct.ProductWarehouse.ProjectFlockKandang, e.DeliveryDate, productFlags, category) var product *productDTO.ProductRelationDTO if e.MarketingProduct.ProductWarehouse.Product.Id != 0 { @@ -131,14 +137,27 @@ func ToPenjualanRealisasiResponseDTO(e []entity.MarketingDeliveryProduct) Penjua } } -func calculateAgeFromChickin(projectFlockKandang *entity.ProjectFlockKandang, deliveryDate *time.Time, productFlags []string) (int, int) { +func calculateAgeFromChickin(projectFlockKandang *entity.ProjectFlockKandang, deliveryDate *time.Time, productFlags []string, category string) (int, int) { if projectFlockKandang == nil || deliveryDate == nil || len(projectFlockKandang.Chickins) == 0 { return 0, 0 } for _, flag := range productFlags { - if flag == "OVK" || flag == "PAKAN" { - return 0, 0 // + if flag == string(utils.FlagOVK) || + flag == string(utils.FlagPakan) || + flag == string(utils.FlagPreStarter) || + flag == string(utils.FlagStarter) || + flag == string(utils.FlagFinisher) || + flag == string(utils.FlagObat) || + flag == string(utils.FlagVitamin) || + flag == string(utils.FlagKimia) || + flag == string(utils.FlagEkspedisi) || + flag == string(utils.FlagTelur) || + flag == string(utils.FlagTelurUtuh) || + flag == string(utils.FlagTelurPecah) || + flag == string(utils.FlagTelurPutih) || + flag == string(utils.FlagTelurRetak) { + return 0, 0 } } @@ -156,8 +175,12 @@ func calculateAgeFromChickin(projectFlockKandang *entity.ProjectFlockKandang, de if ageInDays <= 0 { ageInWeeks = 0 } else { - - ageInWeeks = ((ageInDays - 1) / 7) + 1 + if category == string(utils.ProjectFlockCategoryLaying) { + ageInDays = ageInDays + 119 + ageInWeeks = ((ageInDays - 1) / 7) + 1 + } else { + ageInWeeks = ((ageInDays - 1) / 7) + 1 + } } return ageInDays, ageInWeeks diff --git a/internal/modules/inventory/adjustments/dto/adjustment.dto.go b/internal/modules/inventory/adjustments/dto/adjustment.dto.go index 1ce3da1b..c07f84f9 100644 --- a/internal/modules/inventory/adjustments/dto/adjustment.dto.go +++ b/internal/modules/inventory/adjustments/dto/adjustment.dto.go @@ -103,7 +103,7 @@ func ToProductWarehouseDTO(e *entity.ProductWarehouse) *ProductWarehouseDTO { func ToAdjustmentRelationDTO(e *entity.AdjustmentStock) AdjustmentRelationDTO { return AdjustmentRelationDTO{ Id: e.Id, - Note: e.StockLog.Notes, + Note: "", Increase: e.TotalQty, Decrease: e.UsageQty, ProductWarehouseId: e.ProductWarehouseId, @@ -113,24 +113,17 @@ func ToAdjustmentRelationDTO(e *entity.AdjustmentStock) AdjustmentRelationDTO { func ToAdjustmentListDTO(e *entity.AdjustmentStock) AdjustmentListDTO { var createdUser *userDTO.UserRelationDTO - if e.StockLog != nil && e.StockLog.CreatedUser != nil { - createdUser = &userDTO.UserRelationDTO{ - Id: e.StockLog.CreatedUser.Id, - IdUser: e.StockLog.CreatedUser.IdUser, - Email: e.StockLog.CreatedUser.Email, - Name: e.StockLog.CreatedUser.Name, - } - } - createdAt := time.Time{} - if e.StockLog != nil { - createdAt = e.StockLog.CreatedAt + // Get created user from StockLog + if e.StockLog != nil && e.StockLog.CreatedUser != nil { + mapped := userDTO.ToUserRelationDTO(*e.StockLog.CreatedUser) + createdUser = &mapped } return AdjustmentListDTO{ AdjustmentRelationDTO: ToAdjustmentRelationDTO(e), CreatedUser: createdUser, - CreatedAt: createdAt, + CreatedAt: e.CreatedAt, } } diff --git a/internal/modules/inventory/adjustments/repositories/adjustment_stock.repository.go b/internal/modules/inventory/adjustments/repositories/adjustment_stock.repository.go index fa2685e7..f62738a3 100644 --- a/internal/modules/inventory/adjustments/repositories/adjustment_stock.repository.go +++ b/internal/modules/inventory/adjustments/repositories/adjustment_stock.repository.go @@ -9,7 +9,7 @@ import ( type AdjustmentStockRepository interface { CreateOne(ctx context.Context, data *entity.AdjustmentStock, modifier func(*gorm.DB) *gorm.DB) error - GetByStockLogID(ctx context.Context, stockLogID uint) (*entity.AdjustmentStock, error) + GetByID(ctx context.Context, id uint, modifier func(*gorm.DB) *gorm.DB) (*entity.AdjustmentStock, error) WithTx(tx *gorm.DB) AdjustmentStockRepository DB() *gorm.DB } @@ -30,19 +30,13 @@ func (r *adjustmentStockRepositoryImpl) CreateOne(ctx context.Context, data *ent return q.Create(data).Error } -func (r *adjustmentStockRepositoryImpl) GetByStockLogID(ctx context.Context, stockLogID uint) (*entity.AdjustmentStock, error) { +func (r *adjustmentStockRepositoryImpl) GetByID(ctx context.Context, id uint, modifier func(*gorm.DB) *gorm.DB) (*entity.AdjustmentStock, error) { var record entity.AdjustmentStock - err := r.db.WithContext(ctx). - Preload("StockLog"). - Preload("StockLog.ProductWarehouse"). - Preload("StockLog.ProductWarehouse.Product"). - Preload("StockLog.ProductWarehouse.Warehouse"). - Preload("StockLog.CreatedUser"). - Preload("ProductWarehouse"). - Preload("ProductWarehouse.Product"). - Preload("ProductWarehouse.Warehouse"). - Where("stock_log_id = ?", stockLogID). - First(&record).Error + q := r.db.WithContext(ctx) + if modifier != nil { + q = modifier(q) + } + err := q.First(&record, id).Error if err != nil { return nil, err } diff --git a/internal/modules/inventory/adjustments/services/adjustment.service.go b/internal/modules/inventory/adjustments/services/adjustment.service.go index bec0ef74..16bcf70a 100644 --- a/internal/modules/inventory/adjustments/services/adjustment.service.go +++ b/internal/modules/inventory/adjustments/services/adjustment.service.go @@ -70,11 +70,11 @@ func (s *adjustmentService) withRelations(db *gorm.DB) *gorm.DB { Preload("ProductWarehouse"). Preload("ProductWarehouse.Product"). Preload("ProductWarehouse.Warehouse"). - Preload("CreatedUser") + Preload("StockLog.CreatedUser") } func (s *adjustmentService) GetOne(c *fiber.Ctx, id uint) (*entity.AdjustmentStock, error) { - adjustmentStock, err := s.AdjustmentStockRepository.GetByStockLogID(c.Context(), id) + adjustmentStock, err := s.AdjustmentStockRepository.GetByID(c.Context(), id, s.withRelations) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "Adjustment not found") @@ -164,13 +164,13 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e if transactionType == string(utils.StockLogTransactionTypeIncrease) { afterQuantity += req.Quantity - newLog.Increase = afterQuantity + newLog.Increase = req.Quantity } else { if productWarehouse.Quantity < req.Quantity { return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok tidak mencukupi untuk pengurangan. Stok saat ini: %.2f, Jumlah yang akan dikurangi: %.2f", productWarehouse.Quantity, req.Quantity)) } afterQuantity -= req.Quantity - newLog.Decrease = afterQuantity + newLog.Decrease = req.Quantity } if err := s.StockLogsRepository.WithTx(tx).CreateOne(ctx, newLog, nil); err != nil { @@ -179,7 +179,6 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e } adjustmentStock := &entity.AdjustmentStock{ - StockLogId: newLog.Id, ProductWarehouseId: productWarehouse.Id, } if err := s.AdjustmentStockRepository.WithTx(tx).CreateOne(ctx, adjustmentStock, nil); err != nil { @@ -187,6 +186,12 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e return fiber.NewError(fiber.StatusInternalServerError, "Failed to create adjustment stock record") } + newLog.LoggableType = string(utils.StockLogTypeAdjustment) + newLog.LoggableId = adjustmentStock.Id + if err := s.StockLogsRepository.WithTx(tx).UpdateOne(ctx, newLog.Id, newLog, nil); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to link stock log") + } + if transactionType == string(utils.StockLogTransactionTypeIncrease) { note := fmt.Sprintf("Stock Adjustment IN #%d", newLog.Id) @@ -216,7 +221,6 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e } } - // LEGACY: Update ProductWarehouse quantity (for backward compatibility/reporting) productWarehouse.Quantity = afterQuantity if err := s.ProductWarehouseRepo.WithTx(tx).UpdateOne(ctx, productWarehouse.Id, productWarehouse, nil); err != nil { s.Log.Errorf("Failed to update product warehouse quantity: %+v", err) @@ -295,29 +299,23 @@ func (s *adjustmentService) AdjustmentHistory(c *fiber.Ctx, query *validation.Qu var total int64 q := s.AdjustmentStockRepository.DB().WithContext(c.Context()).Model(&entity.AdjustmentStock{}). - Preload("StockLog"). - Preload("StockLog.ProductWarehouse"). - Preload("StockLog.ProductWarehouse.Product"). - Preload("StockLog.ProductWarehouse.Warehouse"). - Preload("StockLog.CreatedUser"). Preload("ProductWarehouse"). Preload("ProductWarehouse.Product"). - Preload("ProductWarehouse.Warehouse") + Preload("ProductWarehouse.Warehouse"). + Preload("StockLog.CreatedUser") if query.ProductID > 0 { - q = q.Joins("JOIN stock_logs ON stock_logs.id = adjustment_stocks.stock_log_id"). - Joins("JOIN product_warehouses ON product_warehouses.id = stock_logs.product_warehouse_id"). + q = q.Joins("JOIN product_warehouses ON product_warehouses.id = adjustment_stocks.product_warehouse_id"). Where("product_warehouses.product_id = ?", query.ProductID) } if query.WarehouseID > 0 { - q = q.Joins("JOIN stock_logs ON stock_logs.id = adjustment_stocks.stock_log_id"). - Joins("JOIN product_warehouses ON product_warehouses.id = stock_logs.product_warehouse_id"). + q = q.Joins("JOIN product_warehouses ON product_warehouses.id = adjustment_stocks.product_warehouse_id"). Where("product_warehouses.warehouse_id = ?", query.WarehouseID) } if query.TransactionType != "" { - q = q.Joins("JOIN stock_logs ON stock_logs.id = adjustment_stocks.stock_log_id"). + q = q.Joins("JOIN stock_logs ON stock_logs.loggable_type = ? AND stock_logs.loggable_id = adjustment_stocks.id", "ADJUSTMENT"). Where("stock_logs.transaction_type = ?", strings.ToUpper(query.TransactionType)) } diff --git a/internal/modules/inventory/transfers/services/transfer.service.go b/internal/modules/inventory/transfers/services/transfer.service.go index ea1041ea..fe5f8f5a 100644 --- a/internal/modules/inventory/transfers/services/transfer.service.go +++ b/internal/modules/inventory/transfers/services/transfer.service.go @@ -235,6 +235,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques stockTransferDeliveryRepoTX := s.StockTransferDeliveryRepo.WithTx(tx) stockTransferDeliveryItemRepoTX := s.StockTransferDeliveryItemRepo.WithTx(tx) productWarehouseRepoTX := rProductWarehouse.NewProductWarehouseRepository(tx) + stocklogsRepoTx := s.StockLogsRepository.WithTx(tx) if err := stockTransferRepoTX.CreateOne(c.Context(), entityTransfer, nil); err != nil { return err @@ -405,6 +406,19 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques return fiber.NewError(fiber.StatusInternalServerError, "Gagal memperbarui data tracking") } + stockLogDecrease := &entity.StockLog{ + ProductWarehouseId: uint(*detail.SourceProductWarehouseID), + CreatedBy: uint(actorID), + Increase: 0, + Decrease: product.ProductQty, + LoggableType: string(utils.StockLogTypeTransfer), + LoggableId: uint(detail.Id), + Notes: "", + } + if err := stocklogsRepoTx.CreateOne(c.Context(), stockLogDecrease, nil); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat log stok keluar") + } + note := fmt.Sprintf("Transfer #%s", entityTransfer.MovementNumber) replenishResult, err := s.FifoSvc.Replenish(c.Context(), commonSvc.StockReplenishRequest{ StockableKey: fifo.StockableKeyStockTransferIn, @@ -427,6 +441,19 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques s.Log.Errorf("Failed to update tracking total for detail_id=%d, product_id=%d: %+v", detail.Id, product.ProductID, err) return fiber.NewError(fiber.StatusInternalServerError, "Gagal memperbarui data tracking") } + + stockLogIncrease := &entity.StockLog{ + ProductWarehouseId: uint(*detail.DestProductWarehouseID), + CreatedBy: uint(actorID), + Increase: product.ProductQty, + Decrease: 0, + LoggableType: string(utils.StockLogTypeTransfer), + LoggableId: uint(detail.Id), + Notes: "", + } + if err := stocklogsRepoTx.CreateOne(c.Context(), stockLogIncrease, nil); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat log stok masuk") + } } if len(req.Deliveries) > 0 { diff --git a/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go b/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go index 474a53c2..f5b55a78 100644 --- a/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go +++ b/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go @@ -85,6 +85,7 @@ func (r *projectFlockKandangRepositoryImpl) GetByProjectFlockID(ctx context.Cont var records []entity.ProjectFlockKandang if err := r.db.WithContext(ctx). Where("project_flock_id = ?", projectFlockID). + Preload("Kandang"). Find(&records).Error; err != nil { return nil, err } diff --git a/internal/modules/production/transfer_layings/controllers/transfer_laying.controller.go b/internal/modules/production/transfer_layings/controllers/transfer_laying.controller.go index d0ee5061..581b9093 100644 --- a/internal/modules/production/transfer_layings/controllers/transfer_laying.controller.go +++ b/internal/modules/production/transfer_layings/controllers/transfer_laying.controller.go @@ -9,6 +9,7 @@ import ( service "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/services" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/validations" "gitlab.com/mbugroup/lti-api.git/internal/response" + "gitlab.com/mbugroup/lti-api.git/internal/utils" "github.com/gofiber/fiber/v2" ) @@ -28,9 +29,11 @@ func (u *TransferLayingController) GetAll(c *fiber.Ctx) error { Page: c.QueryInt("page", 1), Limit: c.QueryInt("limit", 10), Search: c.Query("search", ""), - TransferDate: c.Query("transfer_date", ""), - FlockSource: uint(c.QueryInt("flock_source", 0)), - FlockDestination: uint(c.QueryInt("flock_destination", 0)), + StartDate: c.Query("start_date", ""), + EndDate: c.Query("end_date", ""), + FlockSource: utils.ParseQueryUintArray(c.Query("flock_source", "")), + FlockDestination: utils.ParseQueryUintArray(c.Query("flock_destination", "")), + Status: utils.ParseQueryArray(c.Query("status", "")), } if query.Page < 1 || query.Limit < 1 { @@ -218,3 +221,29 @@ func (u *TransferLayingController) GetAvailableQtyPerKandang(c *fiber.Ctx) error Data: resp, }) } + +func (u *TransferLayingController) GetMaxTargetQtyPerKandang(c *fiber.Ctx) error { + projectFlockID, err := strconv.ParseUint(c.Params("project_flock_id"), 10, 32) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_id") + } + kandangMaxTargetQty, err := u.TransferLayingService.GetMaxTargetQtyPerKandang(c, uint(projectFlockID)) + if err != nil { + return err + } + + kandangs := make([]dto.KandangMaxTargetQtyDTO, 0, len(kandangMaxTargetQty)) + for pfkId, maxTargetQty := range kandangMaxTargetQty { + kandangs = append(kandangs, dto.ToKandangMaxTargetQtyDTO(pfkId, maxTargetQty)) + } + + resp := dto.ToMaxTargetQtyForTransferDTO(uint(projectFlockID), kandangs) + + return c.Status(fiber.StatusOK). + JSON(response.Success{ + Code: fiber.StatusOK, + Status: "success", + Message: "Get max target quantity successfully", + Data: resp, + }) +} diff --git a/internal/modules/production/transfer_layings/dto/transfer_laying.dto.go b/internal/modules/production/transfer_layings/dto/transfer_laying.dto.go index dfc5e5d9..53e069b2 100644 --- a/internal/modules/production/transfer_layings/dto/transfer_laying.dto.go +++ b/internal/modules/production/transfer_layings/dto/transfer_laying.dto.go @@ -5,6 +5,9 @@ import ( entity "gitlab.com/mbugroup/lti-api.git/internal/entities" approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto" + productWarehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/dto" + kandangDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto" + projectFlockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/dto" userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" ) @@ -17,60 +20,35 @@ type TransferLayingRelationDTO struct { Notes string `json:"notes"` } -type ProjectFlockSummaryDTO struct { - Id uint `json:"id"` - FlockName string `json:"flock_name"` - Category string `json:"category"` -} - -type ProductSummaryDTO struct { - Id uint `json:"id"` - Name string `json:"name"` -} - -type WarehouseSummaryDTO struct { - Id uint `json:"id"` - Name string `json:"name"` - Type string `json:"type"` -} - -type ProductWarehouseSummaryDTO struct { - Product *ProductSummaryDTO `json:"product,omitempty"` - Warehouse *WarehouseSummaryDTO `json:"warehouse,omitempty"` -} - -type ProjectFlockKandangSummaryDTO struct { - Id uint `json:"id"` - Kandang *KandangSummaryDTO `json:"kandang,omitempty"` -} - -type KandangSummaryDTO struct { - Id uint `json:"id"` - Name string `json:"name"` +type ProjectFlockKandangWithKandangDTO struct { + Id uint `json:"id"` + KandangId uint `json:"kandang_id"` + ProjectFlockId uint `json:"project_flock_id"` + Kandang *kandangDTO.KandangRelationDTO `json:"kandang,omitempty"` } type LayingTransferSourceDTO struct { - SourceProjectFlockKandang *ProjectFlockKandangSummaryDTO `json:"source_project_flock_kandang,omitempty"` - Qty float64 `json:"qty"` - ProductWarehouse *ProductWarehouseSummaryDTO `json:"product_warehouse,omitempty"` - Note string `json:"note,omitempty"` + SourceProjectFlockKandang *ProjectFlockKandangWithKandangDTO `json:"source_project_flock_kandang,omitempty"` + Qty float64 `json:"qty"` + ProductWarehouse *productWarehouseDTO.ProductWarehouseRelationDTO `json:"product_warehouse,omitempty"` + Note string `json:"note,omitempty"` } type LayingTransferTargetDTO struct { - TargetProjectFlockKandang *ProjectFlockKandangSummaryDTO `json:"target_project_flock_kandang,omitempty"` - Qty float64 `json:"qty"` - ProductWarehouse *ProductWarehouseSummaryDTO `json:"product_warehouse,omitempty"` - Note string `json:"note,omitempty"` + TargetProjectFlockKandang *ProjectFlockKandangWithKandangDTO `json:"target_project_flock_kandang,omitempty"` + Qty float64 `json:"qty"` + ProductWarehouse *productWarehouseDTO.ProductWarehouseRelationDTO `json:"product_warehouse,omitempty"` + Note string `json:"note,omitempty"` } type TransferLayingListDTO struct { TransferLayingRelationDTO - FromProjectFlock *ProjectFlockSummaryDTO `json:"from_project_flock,omitempty"` - ToProjectFlock *ProjectFlockSummaryDTO `json:"to_project_flock,omitempty"` - CreatedBy uint `json:"created_by"` - CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"` - CreatedAt time.Time `json:"created_at"` - Approval *approvalDTO.ApprovalRelationDTO `json:"approval,omitempty"` + FromProjectFlock *projectFlockDTO.ProjectFlockRelationDTO `json:"from_project_flock,omitempty"` + ToProjectFlock *projectFlockDTO.ProjectFlockRelationDTO `json:"to_project_flock,omitempty"` + CreatedBy uint `json:"created_by"` + CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"` + CreatedAt time.Time `json:"created_at"` + Approval *approvalDTO.ApprovalRelationDTO `json:"approval,omitempty"` } type TransferLayingDetailDTO struct { @@ -94,70 +72,26 @@ type AvailableQtyForTransferDTO struct { Kandangs []KandangAvailableQtyDTO `json:"kandangs"` } +// === Max Target Quantity DTOs === + +type KandangMaxTargetQtyDTO struct { + ProjectFlockKandangId uint `json:"project_flock_kandang_id"` + MaxTargetQty float64 `json:"max_target_qty"` +} + +type MaxTargetQtyForTransferDTO struct { + ProjectFlockId uint `json:"project_flock_id"` + ProjectFlockKandangs []KandangMaxTargetQtyDTO `json:"project_flock_kandangs"` +} + // === Mapper Functions === -func ToProjectFlockSummaryDTO(pf *entity.ProjectFlock) *ProjectFlockSummaryDTO { - if pf == nil || pf.Id == 0 { - return nil - } - - return &ProjectFlockSummaryDTO{ - Id: pf.Id, - FlockName: pf.FlockName, - Category: pf.Category, - } -} - -func ToProjectFlockKandangSummaryDTO(pfk *entity.ProjectFlockKandang) *ProjectFlockKandangSummaryDTO { - if pfk == nil || pfk.Id == 0 { - return nil - } - - var kandang *KandangSummaryDTO - if pfk.Kandang.Id != 0 { - kandang = &KandangSummaryDTO{ - Id: pfk.Kandang.Id, - Name: pfk.Kandang.Name, - } - } - - return &ProjectFlockKandangSummaryDTO{ - Id: pfk.Id, - Kandang: kandang, - } -} - -func ToProductSummaryDTO(product *entity.Product) *ProductSummaryDTO { - if product == nil || product.Id == 0 { - return nil - } - - return &ProductSummaryDTO{ - Id: product.Id, - Name: product.Name, - } -} - -func ToWarehouseSummaryDTO(warehouse *entity.Warehouse) *WarehouseSummaryDTO { - if warehouse == nil || warehouse.Id == 0 { - return nil - } - - return &WarehouseSummaryDTO{ - Id: warehouse.Id, - Name: warehouse.Name, - Type: warehouse.Type, - } -} - -func ToProductWarehouseSummaryDTO(pw *entity.ProductWarehouse) *ProductWarehouseSummaryDTO { - if pw == nil || pw.Id == 0 { - return nil - } - - return &ProductWarehouseSummaryDTO{ - Product: ToProductSummaryDTO(&pw.Product), - Warehouse: ToWarehouseSummaryDTO(&pw.Warehouse), +func ToTransferLayingRelationDTO(e entity.LayingTransfer) TransferLayingRelationDTO { + return TransferLayingRelationDTO{ + Id: e.Id, + TransferNumber: e.TransferNumber, + TransferDate: e.TransferDate, + Notes: e.Notes, } } @@ -172,10 +106,29 @@ func ToLayingTransferSourceDTO(source entity.LayingTransferSource) LayingTransfe displayQty = source.RequestedQty } + var pfkDTO *ProjectFlockKandangWithKandangDTO + if source.SourceProjectFlockKandang != nil && source.SourceProjectFlockKandang.Id != 0 { + pfkDTO = &ProjectFlockKandangWithKandangDTO{ + Id: source.SourceProjectFlockKandang.Id, + KandangId: source.SourceProjectFlockKandang.KandangId, + ProjectFlockId: source.SourceProjectFlockKandang.ProjectFlockId, + } + if source.SourceProjectFlockKandang.Kandang.Id != 0 { + mapped := kandangDTO.ToKandangRelationDTO(source.SourceProjectFlockKandang.Kandang) + pfkDTO.Kandang = &mapped + } + } + + var pwDTO *productWarehouseDTO.ProductWarehouseRelationDTO + if source.ProductWarehouse != nil && source.ProductWarehouse.Id != 0 { + mapped := productWarehouseDTO.ToProductWarehouseRelationDTO(*source.ProductWarehouse) + pwDTO = &mapped + } + return LayingTransferSourceDTO{ - SourceProjectFlockKandang: ToProjectFlockKandangSummaryDTO(source.SourceProjectFlockKandang), + SourceProjectFlockKandang: pfkDTO, Qty: displayQty, - ProductWarehouse: ToProductWarehouseSummaryDTO(source.ProductWarehouse), + ProductWarehouse: pwDTO, Note: source.Note, } } @@ -192,10 +145,29 @@ func ToLayingTransferSourceDTOs(sources []entity.LayingTransferSource) []LayingT } func ToLayingTransferTargetDTO(target entity.LayingTransferTarget) LayingTransferTargetDTO { + var pfkDTO *ProjectFlockKandangWithKandangDTO + if target.TargetProjectFlockKandang != nil && target.TargetProjectFlockKandang.Id != 0 { + pfkDTO = &ProjectFlockKandangWithKandangDTO{ + Id: target.TargetProjectFlockKandang.Id, + KandangId: target.TargetProjectFlockKandang.KandangId, + ProjectFlockId: target.TargetProjectFlockKandang.ProjectFlockId, + } + if target.TargetProjectFlockKandang.Kandang.Id != 0 { + mapped := kandangDTO.ToKandangRelationDTO(target.TargetProjectFlockKandang.Kandang) + pfkDTO.Kandang = &mapped + } + } + + var pwDTO *productWarehouseDTO.ProductWarehouseRelationDTO + if target.ProductWarehouse != nil && target.ProductWarehouse.Id != 0 { + mapped := productWarehouseDTO.ToProductWarehouseRelationDTO(*target.ProductWarehouse) + pwDTO = &mapped + } + return LayingTransferTargetDTO{ - TargetProjectFlockKandang: ToProjectFlockKandangSummaryDTO(target.TargetProjectFlockKandang), - Qty: target.TotalQty, // Ambil dari TotalQty (FIFO replenished quantity) - ProductWarehouse: ToProductWarehouseSummaryDTO(target.ProductWarehouse), + TargetProjectFlockKandang: pfkDTO, + Qty: target.TotalQty, + ProductWarehouse: pwDTO, Note: target.Note, } } @@ -211,15 +183,6 @@ func ToLayingTransferTargetDTOs(targets []entity.LayingTransferTarget) []LayingT return result } -func ToTransferLayingRelationDTO(e entity.LayingTransfer) TransferLayingRelationDTO { - return TransferLayingRelationDTO{ - Id: e.Id, - TransferNumber: e.TransferNumber, - TransferDate: e.TransferDate, - Notes: e.Notes, - } -} - func ToTransferLayingListDTO(e entity.LayingTransfer) TransferLayingListDTO { var createdUser *userDTO.UserRelationDTO if e.CreatedUser != nil && e.CreatedUser.Id != 0 { @@ -227,26 +190,52 @@ func ToTransferLayingListDTO(e entity.LayingTransfer) TransferLayingListDTO { createdUser = &mapped } + var approval *approvalDTO.ApprovalRelationDTO + if e.LatestApproval != nil { + mapped := approvalDTO.ToApprovalDTO(*e.LatestApproval) + approval = &mapped + } + + // Build from project flock DTO + var fromProjectFlock *projectFlockDTO.ProjectFlockRelationDTO + if e.FromProjectFlock != nil && e.FromProjectFlock.Id != 0 { + fromProjectFlock = &projectFlockDTO.ProjectFlockRelationDTO{ + Id: e.FromProjectFlock.Id, + FlockName: e.FromProjectFlock.FlockName, + } + } + + var toProjectFlock *projectFlockDTO.ProjectFlockRelationDTO + if e.ToProjectFlock != nil && e.ToProjectFlock.Id != 0 { + toProjectFlock = &projectFlockDTO.ProjectFlockRelationDTO{ + Id: e.ToProjectFlock.Id, + FlockName: e.ToProjectFlock.FlockName, + } + } + return TransferLayingListDTO{ TransferLayingRelationDTO: ToTransferLayingRelationDTO(e), - FromProjectFlock: ToProjectFlockSummaryDTO(e.FromProjectFlock), - ToProjectFlock: ToProjectFlockSummaryDTO(e.ToProjectFlock), + FromProjectFlock: fromProjectFlock, + ToProjectFlock: toProjectFlock, CreatedBy: e.CreatedBy, CreatedUser: createdUser, CreatedAt: e.CreatedAt, + Approval: approval, } } func ToTransferLayingDetailDTO(e entity.LayingTransfer, approvals []entity.Approval) TransferLayingDetailDTO { var latestApproval *approvalDTO.ApprovalRelationDTO - if e.LatestApproval != nil { - mapped := approvalDTO.ToApprovalDTO(*e.LatestApproval) + // Prioritas: e.LatestApproval > approvals slice + approvalToMap := e.LatestApproval + if approvalToMap == nil && len(approvals) > 0 { + approvalToMap = &approvals[len(approvals)-1] + } + + if approvalToMap != nil { + mapped := approvalDTO.ToApprovalDTO(*approvalToMap) latestApproval = &mapped - } else if len(approvals) > 0 { - // Fallback to approvals slice - latest := approvalDTO.ToApprovalDTO(approvals[len(approvals)-1]) - latestApproval = &latest } return TransferLayingDetailDTO{ @@ -260,13 +249,14 @@ func ToTransferLayingDetailDTO(e entity.LayingTransfer, approvals []entity.Appro func ToTransferLayingDetailDTOWithSingleApproval(e entity.LayingTransfer, approval *entity.Approval) TransferLayingDetailDTO { var mappedApproval *approvalDTO.ApprovalRelationDTO - // Prefer LatestApproval from entity - if e.LatestApproval != nil && e.LatestApproval.Id != 0 { - mapped := approvalDTO.ToApprovalDTO(*e.LatestApproval) - mappedApproval = &mapped - } else if approval != nil && approval.Id != 0 { - // Fallback to passed approval parameter - mapped := approvalDTO.ToApprovalDTO(*approval) + // Prioritas: e.LatestApproval > approval parameter + approvalToMap := e.LatestApproval + if approvalToMap == nil && approval != nil { + approvalToMap = approval + } + + if approvalToMap != nil { + mapped := approvalDTO.ToApprovalDTO(*approvalToMap) mappedApproval = &mapped } @@ -285,3 +275,17 @@ func ToTransferLayingListDTOs(items []entity.LayingTransfer) []TransferLayingLis } return result } + +func ToKandangMaxTargetQtyDTO(pfkId uint, maxTargetQTY float64) KandangMaxTargetQtyDTO { + return KandangMaxTargetQtyDTO{ + ProjectFlockKandangId: uint(pfkId), + MaxTargetQty: maxTargetQTY, + } +} + +func ToMaxTargetQtyForTransferDTO(pfId uint, kandangs []KandangMaxTargetQtyDTO) MaxTargetQtyForTransferDTO { + return MaxTargetQtyForTransferDTO{ + ProjectFlockId: pfId, + ProjectFlockKandangs: kandangs, + } +} diff --git a/internal/modules/production/transfer_layings/repositories/laying_transfer.repository.go b/internal/modules/production/transfer_layings/repositories/laying_transfer.repository.go index 3dab5120..14fa4118 100644 --- a/internal/modules/production/transfer_layings/repositories/laying_transfer.repository.go +++ b/internal/modules/production/transfer_layings/repositories/laying_transfer.repository.go @@ -2,6 +2,7 @@ package repository import ( "context" + "strings" "gitlab.com/mbugroup/lti-api.git/internal/common/repository" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" @@ -12,6 +13,9 @@ type TransferLayingRepository interface { repository.BaseRepository[entity.LayingTransfer] GetByTransferNumber(ctx context.Context, transferNumber string) (*entity.LayingTransfer, error) IdExists(ctx context.Context, id uint) (bool, error) + + // Tambah method baru untuk query dengan filter lengkap + GetAllWithFilters(ctx context.Context, offset int, limit int, params *GetAllFilterParams) ([]entity.LayingTransfer, int64, error) } type TransferLayingRepositoryImpl struct { @@ -40,3 +44,93 @@ func (r *TransferLayingRepositoryImpl) GetByTransferNumber(ctx context.Context, } return &transfer, nil } + +type GetAllFilterParams struct { + Search string + StartDate string + EndDate string + FlockSource []uint + FlockDestination []uint + Status []string +} + +func (r *TransferLayingRepositoryImpl) GetAllWithFilters(ctx context.Context, offset int, limit int, params *GetAllFilterParams) ([]entity.LayingTransfer, int64, error) { + var records []entity.LayingTransfer + var total int64 + + q := r.db.WithContext(ctx).Model(&entity.LayingTransfer{}) + + if params.Search != "" { + searchPattern := "%" + params.Search + "%" + q = q.Joins("LEFT JOIN project_flocks AS pf_from ON laying_transfers.from_project_flock_id = pf_from.id"). + Joins("LEFT JOIN project_flocks AS pf_to ON laying_transfers.to_project_flock_id = pf_to.id"). + Where("laying_transfers.transfer_number ILIKE ? OR laying_transfers.notes ILIKE ? OR pf_from.flock_name ILIKE ? OR pf_to.flock_name ILIKE ?", + searchPattern, searchPattern, searchPattern, searchPattern) + } + + if params.StartDate != "" && params.EndDate != "" { + q = q.Where("transfer_date::date >= ?::date AND transfer_date::date <= ?::date", + params.StartDate, params.EndDate) + } else if params.StartDate != "" { + q = q.Where("transfer_date::date >= ?::date", params.StartDate) + } else if params.EndDate != "" { + q = q.Where("transfer_date::date <= ?::date", params.EndDate) + } + + if len(params.FlockSource) > 0 { + q = q.Where("from_project_flock_id IN ?", params.FlockSource) + } + + if len(params.FlockDestination) > 0 { + q = q.Where("to_project_flock_id IN ?", params.FlockDestination) + } + + if len(params.Status) > 0 { + statusConditions := []string{} + statusValues := []interface{}{} + + for _, status := range params.Status { + switch status { + case "PENDING": + statusConditions = append(statusConditions, + "NOT EXISTS (SELECT 1 FROM approvals WHERE approvable_type = 'TRANSFER_TO_LAYINGS' AND approvable_id = laying_transfers.id)") + + case "APPROVED": + statusConditions = append(statusConditions, + "EXISTS (SELECT 1 FROM approvals WHERE approvable_type = 'TRANSFER_TO_LAYINGS' AND approvable_id = laying_transfers.id AND action = 'APPROVED' ORDER BY created_at DESC LIMIT 1)") + + case "REJECTED": + statusConditions = append(statusConditions, + "EXISTS (SELECT 1 FROM approvals WHERE approvable_type = 'TRANSFER_TO_LAYINGS' AND approvable_id = laying_transfers.id AND action = 'REJECTED' ORDER BY created_at DESC LIMIT 1)") + } + } + + if len(statusConditions) > 0 { + q = q.Where("("+strings.Join(statusConditions, " OR ")+")", statusValues...) + } + } + + if err := q.Count(&total).Error; err != nil { + return nil, 0, err + } + + q = q.Offset(offset).Limit(limit). + Preload("FromProjectFlock"). + Preload("ToProjectFlock"). + Preload("CreatedUser"). + Preload("Sources"). + Preload("Sources.SourceProjectFlockKandang"). + Preload("Sources.SourceProjectFlockKandang.Kandang"). + Preload("Sources.ProductWarehouse"). + Preload("Targets"). + Preload("Targets.TargetProjectFlockKandang"). + Preload("Targets.TargetProjectFlockKandang.Kandang"). + Preload("Targets.ProductWarehouse"). + Order("laying_transfers.created_at DESC") + + if err := q.Find(&records).Error; err != nil { + return nil, 0, err + } + + return records, total, nil +} diff --git a/internal/modules/production/transfer_layings/route.go b/internal/modules/production/transfer_layings/route.go index 8f7a62c0..c16ba1a8 100644 --- a/internal/modules/production/transfer_layings/route.go +++ b/internal/modules/production/transfer_layings/route.go @@ -21,11 +21,12 @@ func TransferLayingRoutes(v1 fiber.Router, u user.UserService, s transferLaying. // route.Delete("/:id", m.Auth(u), ctrl.DeleteOne) // route.Post("/approval", m.Auth(u), ctrl.Approval) - route.Get("/",m.RequirePermissions(m.P_TransferToLaying_GetAll), ctrl.GetAll) - route.Post("/",m.RequirePermissions(m.P_TransferToLaying_CreateOne), ctrl.CreateOne) - route.Get("/:id",m.RequirePermissions(m.P_TransferToLaying_GetOne), ctrl.GetOne) - route.Patch("/:id",m.RequirePermissions(m.P_TransferToLaying_UpdateOne), ctrl.UpdateOne) - route.Delete("/:id",m.RequirePermissions(m.P_TransferToLaying_DeleteOne), ctrl.DeleteOne) - route.Post("/approvals",m.RequirePermissions(m.P_TransferToLaying_Approval), ctrl.Approval) - route.Get("/project-flocks/:project_flock_id/available-qty",m.RequirePermissions(m.P_TransferToLaying_GetAvailableQty), ctrl.GetAvailableQtyPerKandang) + route.Get("/", m.RequirePermissions(m.P_TransferToLaying_GetAll), ctrl.GetAll) + route.Post("/", m.RequirePermissions(m.P_TransferToLaying_CreateOne), ctrl.CreateOne) + route.Get("/:id", m.RequirePermissions(m.P_TransferToLaying_GetOne), ctrl.GetOne) + route.Patch("/:id", m.RequirePermissions(m.P_TransferToLaying_UpdateOne), ctrl.UpdateOne) + route.Delete("/:id", m.RequirePermissions(m.P_TransferToLaying_DeleteOne), ctrl.DeleteOne) + route.Post("/approvals", m.RequirePermissions(m.P_TransferToLaying_Approval), ctrl.Approval) + route.Get("/project-flocks/:project_flock_id/available-qty", m.RequirePermissions(m.P_TransferToLaying_GetAvailableQty), ctrl.GetAvailableQtyPerKandang) + route.Get("/project-flocks/:project_flock_id/max-target-qty", m.RequirePermissions(m.P_TransferToLaying_CreateOne), ctrl.GetMaxTargetQtyPerKandang) } diff --git a/internal/modules/production/transfer_layings/services/transfer_laying.service.go b/internal/modules/production/transfer_layings/services/transfer_laying.service.go index 8e0269cf..a5d0ba88 100644 --- a/internal/modules/production/transfer_layings/services/transfer_laying.service.go +++ b/internal/modules/production/transfer_layings/services/transfer_laying.service.go @@ -34,6 +34,7 @@ type TransferLayingService interface { DeleteOne(ctx *fiber.Ctx, id uint) error Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.LayingTransfer, error) GetAvailableQtyPerKandang(ctx *fiber.Ctx, projectFlockID uint) (*entity.ProjectFlock, map[uint]float64, error) + GetMaxTargetQtyPerKandang(ctx *fiber.Ctx, projectFlockID uint) (map[uint]float64, error) } type transferLayingService struct { @@ -108,34 +109,20 @@ func (s transferLayingService) GetAll(c *fiber.Ctx, params *validation.Query) ([ offset := (params.Page - 1) * params.Limit - transferLayings, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { - // Apply search and filters - if params.Search != "" { - searchPattern := "%" + params.Search + "%" - db = db.Joins("LEFT JOIN project_flocks AS pf_from ON laying_transfers.from_project_flock_id = pf_from.id"). - Joins("LEFT JOIN project_flocks AS pf_to ON laying_transfers.to_project_flock_id = pf_to.id"). - Where("laying_transfers.transfer_number ILIKE ? OR laying_transfers.notes ILIKE ? OR pf_from.flock_name ILIKE ? OR pf_to.flock_name ILIKE ?", - searchPattern, searchPattern, searchPattern, searchPattern) - } + filterParams := &repository.GetAllFilterParams{ + Search: params.Search, + StartDate: params.StartDate, + EndDate: params.EndDate, + FlockSource: params.FlockSource, + FlockDestination: params.FlockDestination, + Status: params.Status, + } - if params.TransferDate != "" { - db = db.Where("transfer_date::date = ?::date", params.TransferDate) - } - - if params.FlockSource > 0 { - db = db.Where("from_project_flock_id = ?", params.FlockSource) - } - - if params.FlockDestination > 0 { - db = db.Where("to_project_flock_id = ?", params.FlockDestination) - } - - db = db.Order("created_at DESC") - - db = s.withRelations(db) - - return db - }) + transferLayings, total, err := s.Repository.GetAllWithFilters(c.Context(), offset, params.Limit, filterParams) + if err != nil { + s.Log.Errorf("Failed to get transferLayings: %+v", err) + return nil, 0, err + } if err != nil { s.Log.Errorf("Failed to get transferLayings: %+v", err) @@ -888,3 +875,39 @@ func (s *transferLayingService) validateKandangOwnership( return nil } + +func (s transferLayingService) GetMaxTargetQtyPerKandang(c *fiber.Ctx, projectFlockID uint) (map[uint]float64, error) { + + if err := commonSvc.EnsureRelations(c.Context(), + commonSvc.RelationCheck{Name: "Project Flock", ID: &projectFlockID, Exists: s.ProjectFlockRepo.IdExists}, + ); err != nil { + return nil, err + } + + projectFlockKandangs, err := s.ProjectFlockKandangRepo.GetByProjectFlockID(c.Context(), projectFlockID) + if err != nil { + return nil, err + } + + kandangMaxTargetQty := make(map[uint]float64) + for _, projectFlockKandang := range projectFlockKandangs { + + capacity := projectFlockKandang.Kandang.Capacity + + availableQty, err := s.ProjectFlockPopulationRepo.GetAvailableQtyByProjectFlockKandangID( + c.Context(), + projectFlockKandang.Id, + ) + if err != nil { + return nil, err + } + + kandangMaxTargetQty[projectFlockKandang.Id] = capacity - availableQty + + if kandangMaxTargetQty[projectFlockKandang.Id] < 0 { + kandangMaxTargetQty[projectFlockKandang.Id] = 0 + } + } + + return kandangMaxTargetQty, nil +} diff --git a/internal/modules/production/transfer_layings/validations/transfer_laying.validation.go b/internal/modules/production/transfer_layings/validations/transfer_laying.validation.go index 06d52316..0472ba39 100644 --- a/internal/modules/production/transfer_layings/validations/transfer_laying.validation.go +++ b/internal/modules/production/transfer_layings/validations/transfer_laying.validation.go @@ -29,12 +29,14 @@ type Update struct { } type Query struct { - Page int `query:"page" validate:"omitempty,number,min=1,gt=0"` - Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"` - Search string `query:"search" validate:"omitempty"` - TransferDate string `query:"transfer_date" validate:"omitempty"` - FlockSource uint `query:"flock_source" validate:"omitempty,number"` - FlockDestination uint `query:"flock_destination" validate:"omitempty,number"` + Page int `query:"page" validate:"omitempty,number,min=1,gt=0"` + Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"` + Search string `query:"search" validate:"omitempty"` + StartDate string `query:"start_date" validate:"omitempty"` + EndDate string `query:"end_date" validate:"omitempty"` + FlockSource []uint `query:"flock_source" validate:"omitempty"` + FlockDestination []uint `query:"flock_destination" validate:"omitempty"` + Status []string `query:"status" validate:"omitempty"` } type Approve struct { diff --git a/internal/utils/strings.go b/internal/utils/strings.go index a58ba1ac..e9e23f84 100644 --- a/internal/utils/strings.go +++ b/internal/utils/strings.go @@ -2,6 +2,7 @@ package utils import ( "sort" + "strconv" "strings" ) @@ -47,3 +48,54 @@ func ParseFlags(raw string) []string { sort.Strings(res) return res } + +// ParseQueryArray parses comma-separated string values and returns a slice of trimmed strings +// Example: "a, b, c" → ["a", "b", "c"] +func ParseQueryArray(raw string) []string { + if raw == "" { + return nil + } + + parts := strings.Split(raw, ",") + result := make([]string, 0, len(parts)) + + for _, p := range parts { + trimmed := strings.TrimSpace(p) + if trimmed != "" { + result = append(result, trimmed) + } + } + + if len(result) == 0 { + return nil + } + return result +} + +// ParseQueryUintArray parses comma-separated string values and returns a slice of uint +// Invalid values are skipped +// Example: "1, 2, 3" → [1, 2, 3] +func ParseQueryUintArray(raw string) []uint { + if raw == "" { + return nil + } + + parts := strings.Split(raw, ",") + result := make([]uint, 0, len(parts)) + + for _, p := range parts { + trimmed := strings.TrimSpace(p) + if trimmed == "" { + continue + } + + if num, err := strconv.ParseUint(trimmed, 10, 32); err == nil { + result = append(result, uint(num)) + } + } + + if len(result) == 0 { + return nil + } + return result +}