feat[BE-127]: create available qty API and inisiate sales order and delivery order

This commit is contained in:
aguhh18
2025-11-07 13:24:48 +07:00
parent ba12320d12
commit 6e69e97d26
24 changed files with 695 additions and 68 deletions
+26
View File
@@ -0,0 +1,26 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type Marketing struct {
Id uint `gorm:"primaryKey;autoIncrement"`
SoNumber string `gorm:"uniqueIndex;not null"`
CustomerId uint `gorm:"not null"`
SoDocs string `gorm:"type:varchar(20)"`
SoDate time.Time `gorm:"type:date;not null"`
SalesPersonId uint `gorm:"not null"`
Notes string `gorm:"type:text"`
CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
Customer Customer `gorm:"foreignKey:CustomerId;references:Id"`
SalesPerson User `gorm:"foreignKey:SalesPersonId;references:Id"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
Products []MarketingProduct `gorm:"foreignKey:MarketingId;references:Id"`
}
@@ -0,0 +1,24 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type MarketingDeliveryProduct struct {
Id uint `gorm:"primaryKey;autoIncrement"`
MarketingProductId uint `gorm:"uniqueIndex;not null"`
Qty float64 `gorm:"type:numeric(15,3);not null"`
UnitPrice float64 `gorm:"type:numeric(15,3);not null"`
TotalWeight float64 `gorm:"type:numeric(15,3);not null"`
AvgWeight float64 `gorm:"type:numeric(15,3);not null"`
TotalPrice float64 `gorm:"type:numeric(15,3);not null"`
DeliveryDate *time.Time `gorm:"type:timestamptz"`
VehicleNumber string `gorm:"type:varchar(50)"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
MarketingProduct MarketingProduct `gorm:"foreignKey:MarketingProductId;references:Id"`
}
+24
View File
@@ -0,0 +1,24 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type MarketingProduct struct {
Id uint `gorm:"primaryKey;autoIncrement"`
MarketingId uint `gorm:"not null"`
ProductWarehouseId uint `gorm:"not null"`
Qty float64 `gorm:"type:numeric(15,3);not null"`
UnitPrice float64 `gorm:"type:numeric(15,3);not null"`
AvgWeight float64 `gorm:"type:numeric(15,3);not null"`
TotalWeight float64 `gorm:"type:numeric(15,3);not null"`
TotalPrice float64 `gorm:"type:numeric(15,3);not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
Marketing Marketing `gorm:"foreignKey:MarketingId;references:Id"`
ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
}
+18
View File
@@ -0,0 +1,18 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type SalesOrders struct {
Id uint `gorm:"primaryKey"`
Name string `gorm:"not null;uniqueIndex:idx_name,where:deleted_at IS NULL"`
CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
}
+13
View File
@@ -0,0 +1,13 @@
package marketing
import (
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
)
type MarketingModule struct{}
func (MarketingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
RegisterRoutes(router, db, validate)
}
+25
View File
@@ -0,0 +1,25 @@
package marketing
import (
"gitlab.com/mbugroup/lti-api.git/internal/modules"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
salesOrderss "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders"
// MODULE IMPORTS
)
func RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
group := router.Group("/marketing")
allModules := []modules.Module{
salesOrderss.SalesOrdersModule{},
// MODULE REGISTRY
}
for _, m := range allModules {
m.RegisterRoutes(group, db, validate)
}
}
@@ -0,0 +1,144 @@
package controller
import (
"math"
"strconv"
"gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/dto"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/services"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/validations"
"gitlab.com/mbugroup/lti-api.git/internal/response"
"github.com/gofiber/fiber/v2"
)
type SalesOrdersController struct {
SalesOrdersService service.SalesOrdersService
}
func NewSalesOrdersController(salesOrdersService service.SalesOrdersService) *SalesOrdersController {
return &SalesOrdersController{
SalesOrdersService: salesOrdersService,
}
}
func (u *SalesOrdersController) GetAll(c *fiber.Ctx) error {
query := &validation.Query{
Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10),
Search: c.Query("search", ""),
}
if query.Page < 1 || query.Limit < 1 {
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
}
result, totalResults, err := u.SalesOrdersService.GetAll(c, query)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.SalesOrdersListDTO]{
Code: fiber.StatusOK,
Status: "success",
Message: "Get all salesOrderss successfully",
Meta: response.Meta{
Page: query.Page,
Limit: query.Limit,
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
TotalResults: totalResults,
},
Data: dto.ToSalesOrdersListDTOs(result),
})
}
func (u *SalesOrdersController) GetOne(c *fiber.Ctx) error {
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
result, err := u.SalesOrdersService.GetOne(c, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get salesOrders successfully",
Data: dto.ToSalesOrdersListDTO(*result),
})
}
func (u *SalesOrdersController) CreateOne(c *fiber.Ctx) error {
req := new(validation.Create)
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
// result, err := u.SalesOrdersService.CreateOne(c, req)
// if err != nil {
// return err
// }
return c.Status(fiber.StatusCreated).
JSON(response.Success{
Code: fiber.StatusCreated,
Status: "success",
Message: "Create salesOrders successfully",
Data: req,
})
}
func (u *SalesOrdersController) UpdateOne(c *fiber.Ctx) error {
req := new(validation.Update)
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
result, err := u.SalesOrdersService.UpdateOne(c, req, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Update salesOrders successfully",
Data: dto.ToSalesOrdersListDTO(*result),
})
}
func (u *SalesOrdersController) DeleteOne(c *fiber.Ctx) error {
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
if err := u.SalesOrdersService.DeleteOne(c, uint(id)); err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Common{
Code: fiber.StatusOK,
Status: "success",
Message: "Delete salesOrders successfully",
})
}
@@ -0,0 +1,64 @@
package dto
import (
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
)
// === DTO Structs ===
type SalesOrdersBaseDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
}
type SalesOrdersListDTO struct {
SalesOrdersBaseDTO
CreatedUser *userDTO.UserBaseDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type SalesOrdersDetailDTO struct {
SalesOrdersListDTO
}
// === Mapper Functions ===
func ToSalesOrdersBaseDTO(e entity.SalesOrders) SalesOrdersBaseDTO {
return SalesOrdersBaseDTO{
Id: e.Id,
Name: e.Name,
}
}
func ToSalesOrdersListDTO(e entity.SalesOrders) SalesOrdersListDTO {
var createdUser *userDTO.UserBaseDTO
if e.CreatedUser.Id != 0 {
mapped := userDTO.ToUserBaseDTO(e.CreatedUser)
createdUser = &mapped
}
return SalesOrdersListDTO{
SalesOrdersBaseDTO: ToSalesOrdersBaseDTO(e),
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
CreatedUser: createdUser,
}
}
func ToSalesOrdersListDTOs(e []entity.SalesOrders) []SalesOrdersListDTO {
result := make([]SalesOrdersListDTO, len(e))
for i, r := range e {
result[i] = ToSalesOrdersListDTO(r)
}
return result
}
func ToSalesOrdersDetailDTO(e entity.SalesOrders) SalesOrdersDetailDTO {
return SalesOrdersDetailDTO{
SalesOrdersListDTO: ToSalesOrdersListDTO(e),
}
}
@@ -0,0 +1,26 @@
package sales_orders
import (
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
rSalesOrders "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories"
sSalesOrders "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/services"
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
)
type SalesOrdersModule struct{}
func (SalesOrdersModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
salesOrdersRepo := rSalesOrders.NewSalesOrdersRepository(db)
userRepo := rUser.NewUserRepository(db)
salesOrdersService := sSalesOrders.NewSalesOrdersService(salesOrdersRepo, validate)
userService := sUser.NewUserService(userRepo, validate)
SalesOrdersRoutes(router, userService, salesOrdersService)
}
@@ -0,0 +1,21 @@
package repository
import (
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
"gorm.io/gorm"
)
type SalesOrdersRepository interface {
repository.BaseRepository[entity.SalesOrders]
}
type SalesOrdersRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.SalesOrders]
}
func NewSalesOrdersRepository(db *gorm.DB) SalesOrdersRepository {
return &SalesOrdersRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.SalesOrders](db),
}
}
@@ -0,0 +1,28 @@
package sales_orders
import (
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/controllers"
salesOrders "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
"github.com/gofiber/fiber/v2"
)
func SalesOrdersRoutes(v1 fiber.Router, u user.UserService, s salesOrders.SalesOrdersService) {
ctrl := controller.NewSalesOrdersController(s)
route := v1.Group("/sales-orders")
// route.Get("/", m.Auth(u), ctrl.GetAll)
// route.Post("/", m.Auth(u), ctrl.CreateOne)
// route.Get("/:id", m.Auth(u), ctrl.GetOne)
// route.Patch("/:id", m.Auth(u), ctrl.UpdateOne)
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
route.Get("/", ctrl.GetAll)
route.Post("/", ctrl.CreateOne)
route.Get("/:id", ctrl.GetOne)
route.Patch("/:id", ctrl.UpdateOne)
route.Delete("/:id", ctrl.DeleteOne)
}
@@ -0,0 +1,129 @@
package service
import (
"errors"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/validations"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
)
type SalesOrdersService interface {
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.SalesOrders, int64, error)
GetOne(ctx *fiber.Ctx, id uint) (*entity.SalesOrders, error)
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.SalesOrders, error)
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.SalesOrders, error)
DeleteOne(ctx *fiber.Ctx, id uint) error
}
type salesOrdersService struct {
Log *logrus.Logger
Validate *validator.Validate
Repository repository.SalesOrdersRepository
}
func NewSalesOrdersService(repo repository.SalesOrdersRepository, validate *validator.Validate) SalesOrdersService {
return &salesOrdersService{
Log: utils.Log,
Validate: validate,
Repository: repo,
}
}
func (s salesOrdersService) withRelations(db *gorm.DB) *gorm.DB {
return db.Preload("CreatedUser")
}
func (s salesOrdersService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.SalesOrders, int64, error) {
if err := s.Validate.Struct(params); err != nil {
return nil, 0, err
}
offset := (params.Page - 1) * params.Limit
salesOrderss, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db)
if params.Search != "" {
return db.Where("name LIKE ?", "%"+params.Search+"%")
}
return db.Order("created_at DESC").Order("updated_at DESC")
})
if err != nil {
s.Log.Errorf("Failed to get salesOrderss: %+v", err)
return nil, 0, err
}
return salesOrderss, total, nil
}
func (s salesOrdersService) GetOne(c *fiber.Ctx, id uint) (*entity.SalesOrders, error) {
salesOrders, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "SalesOrders not found")
}
if err != nil {
s.Log.Errorf("Failed get salesOrders by id: %+v", err)
return nil, err
}
return salesOrders, nil
}
func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.SalesOrders, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
createBody := &entity.SalesOrders{
Name: req.Name,
}
if err := s.Repository.CreateOne(c.Context(), createBody, nil); err != nil {
s.Log.Errorf("Failed to create salesOrders: %+v", err)
return nil, err
}
return s.GetOne(c, createBody.Id)
}
func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.SalesOrders, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
updateBody := make(map[string]any)
if req.Name != nil {
updateBody["name"] = *req.Name
}
if len(updateBody) == 0 {
return s.GetOne(c, id)
}
if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "SalesOrders not found")
}
s.Log.Errorf("Failed to update salesOrders: %+v", err)
return nil, err
}
return s.GetOne(c, id)
}
func (s salesOrdersService) DeleteOne(c *fiber.Ctx, id uint) error {
if err := s.Repository.DeleteOne(c.Context(), id); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, "SalesOrders not found")
}
s.Log.Errorf("Failed to delete salesOrders: %+v", err)
return err
}
return nil
}
@@ -0,0 +1,15 @@
package validation
type Create struct {
Name string `json:"name" validate:"required_strict,min=3"`
}
type Update struct {
Name *string `json:"name,omitempty" validate:"omitempty"`
}
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,max=50"`
}
@@ -71,6 +71,10 @@ func (u *ProjectflockController) GetAll(c *fiber.Ctx) error {
if period := c.QueryInt("period", 0); period > 0 { if period := c.QueryInt("period", 0); period > 0 {
query.Period = period query.Period = period
} }
if category := c.Query("category", ""); category != "" {
query.Category = category
}
if kandangRaw := c.Query("kandang_id", c.Query("kandang_ids", "")); kandangRaw != "" { if kandangRaw := c.Query("kandang_id", c.Query("kandang_ids", "")); kandangRaw != "" {
ids, err := parseUintList(kandangRaw) ids, err := parseUintList(kandangRaw)
@@ -14,6 +14,7 @@ type ProjectFlockPopulationRepository interface {
ExistsByProjectChickinID(ctx context.Context, projectChickinID uint) (bool, error) ExistsByProjectChickinID(ctx context.Context, projectChickinID uint) (bool, error)
GetByProjectChickinIDAndProductWarehouseID(ctx context.Context, projectChickinID uint, productWarehouseID uint) ([]entity.ProjectFlockPopulation, error) GetByProjectChickinIDAndProductWarehouseID(ctx context.Context, projectChickinID uint, productWarehouseID uint) ([]entity.ProjectFlockPopulation, error)
GetByProjectFlockKandangIDAndProductWarehouseID(ctx context.Context, projectFlockKandangID uint, productWarehouseID uint) ([]entity.ProjectFlockPopulation, error) GetByProjectFlockKandangIDAndProductWarehouseID(ctx context.Context, projectFlockKandangID uint, productWarehouseID uint) ([]entity.ProjectFlockPopulation, error)
GetTotalQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error)
// subset of base repository methods used by services // subset of base repository methods used by services
CreateOne(ctx context.Context, entity *entity.ProjectFlockPopulation, modifier func(*gorm.DB) *gorm.DB) error CreateOne(ctx context.Context, entity *entity.ProjectFlockPopulation, modifier func(*gorm.DB) *gorm.DB) error
@@ -91,3 +92,17 @@ func (r *projectFlockPopulationRepositoryImpl) GetByProjectFlockKandangIDAndProd
} }
return records, nil return records, nil
} }
func (r *projectFlockPopulationRepositoryImpl) GetTotalQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error) {
var total float64
err := r.DB().WithContext(ctx).
Table("project_flock_populations").
Select("COALESCE(SUM(total_qty), 0) AS total_qty").
Joins("JOIN project_chickins ON project_chickins.id = project_flock_populations.project_chickin_id").
Where("project_chickins.project_flock_kandang_id = ?", projectFlockKandangID).
Scan(&total).Error
if err != nil {
return 0, err
}
return total, nil
}
@@ -127,6 +127,9 @@ func (r *ProjectflockRepositoryImpl) applyQueryFilters(db *gorm.DB, params *vali
return db return db
} }
if params.Category != "" {
db = db.Where("project_flocks.category = ?", params.Category)
}
if params.AreaId > 0 { if params.AreaId > 0 {
db = db.Where("project_flocks.area_id = ?", params.AreaId) db = db.Where("project_flocks.area_id = ?", params.AreaId)
} }
@@ -28,5 +28,5 @@ func ProjectflockRoutes(v1 fiber.Router, u user.UserService, s projectflock.Proj
route.Get("/kandangs/lookup", ctrl.LookupProjectFlockKandang) route.Get("/kandangs/lookup", ctrl.LookupProjectFlockKandang)
route.Post("/approvals", ctrl.Approval) route.Post("/approvals", ctrl.Approval)
route.Get("/kandangs/:project-flock_kandang-id/periods", ctrl.GetFlockPeriodSummary) route.Get("/kandangs/:project-flock_kandang-id/periods", ctrl.GetFlockPeriodSummary)
} }
@@ -27,6 +27,7 @@ type Query struct {
AreaId uint `query:"area_id" validate:"omitempty,number,gt=0"` AreaId uint `query:"area_id" validate:"omitempty,number,gt=0"`
LocationId uint `query:"location_id" validate:"omitempty,number,gt=0"` LocationId uint `query:"location_id" validate:"omitempty,number,gt=0"`
Period int `query:"period" validate:"omitempty,number,gt=0"` Period int `query:"period" validate:"omitempty,number,gt=0"`
Category string `query:"category" validate:"omitempty"`
KandangIds []uint `query:"kandang_id" validate:"omitempty,dive,gt=0"` KandangIds []uint `query:"kandang_id" validate:"omitempty,dive,gt=0"`
} }
@@ -1,6 +1,7 @@
package controller package controller
import ( import (
"fmt"
"math" "math"
"strconv" "strconv"
@@ -24,16 +25,8 @@ func NewTransferLayingController(transferLayingService service.TransferLayingSer
func (u *TransferLayingController) GetAll(c *fiber.Ctx) error { func (u *TransferLayingController) GetAll(c *fiber.Ctx) error {
query := &validation.Query{ query := &validation.Query{
Page: c.QueryInt("page", 1), Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10), Limit: c.QueryInt("limit", 10),
SourceProjectFlockId: uint(c.QueryInt("source_project_flock_id", 0)),
TargetProjectFlockId: uint(c.QueryInt("target_project_flock_id", 0)),
TransferDateFrom: c.Query("transfer_date_from", ""),
TransferDateTo: c.Query("transfer_date_to", ""),
ApprovalStatus: c.Query("approval_status", ""),
TransferNumber: c.Query("transfer_number", ""),
Sort: c.Query("sort", "created_at"),
Order: c.Query("order", "desc"),
} }
if query.Page < 1 || query.Limit < 1 { if query.Page < 1 || query.Limit < 1 {
@@ -45,8 +38,13 @@ func (u *TransferLayingController) GetAll(c *fiber.Ctx) error {
return err return err
} }
data := make([]dto.TransferLayingDetailDTO, len(result))
for i, transfer := range result {
data[i] = dto.ToTransferLayingDetailDTOWithSingleApproval(transfer, transfer.LatestApproval)
}
return c.Status(fiber.StatusOK). return c.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.TransferLayingListDTO]{ JSON(response.SuccessWithPaginate[dto.TransferLayingDetailDTO]{
Code: fiber.StatusOK, Code: fiber.StatusOK,
Status: "success", Status: "success",
Message: "Get all transferLayings successfully", Message: "Get all transferLayings successfully",
@@ -56,7 +54,7 @@ func (u *TransferLayingController) GetAll(c *fiber.Ctx) error {
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))), TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
TotalResults: totalResults, TotalResults: totalResults,
}, },
Data: dto.ToTransferLayingListDTOs(result), Data: data,
}) })
} }
@@ -113,7 +111,7 @@ func (u *TransferLayingController) UpdateOne(c *fiber.Ctx) error {
} }
if err := c.BodyParser(req); err != nil { if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid request body: %s", err.Error()))
} }
result, err := u.TransferLayingService.UpdateOne(c, req, uint(id)) result, err := u.TransferLayingService.UpdateOne(c, req, uint(id))
@@ -126,7 +124,7 @@ func (u *TransferLayingController) UpdateOne(c *fiber.Ctx) error {
Code: fiber.StatusOK, Code: fiber.StatusOK,
Status: "success", Status: "success",
Message: "Update transferLaying successfully", Message: "Update transferLaying successfully",
Data: dto.ToTransferLayingListDTO(*result), Data: dto.ToTransferLayingDetailDTOWithSingleApproval(*result, result.LatestApproval),
}) })
} }
@@ -180,3 +178,40 @@ func (u *TransferLayingController) Approval(c *fiber.Ctx) error {
Data: data, Data: data,
}) })
} }
func (u *TransferLayingController) GetAvailableQtyPerKandang(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")
}
pf, kandangQtyMap, err := u.TransferLayingService.GetAvailableQtyPerKandang(c, uint(projectFlockID))
if err != nil {
return err
}
// Build kandang list response
kandangs := make([]dto.KandangAvailableQtyDTO, 0, len(kandangQtyMap))
for kandangPFID, qty := range kandangQtyMap {
kandangs = append(kandangs, dto.KandangAvailableQtyDTO{
ProjectFlockKandangId: kandangPFID,
AvailableQty: qty,
})
}
resp := dto.AvailableQtyForTransferDTO{
ProjectFlockId: pf.Id,
ProjectFlockCode: pf.FlockName,
Category: pf.Category,
Kandangs: kandangs,
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get available quantity successfully",
Data: resp,
})
}
@@ -77,6 +77,20 @@ type TransferLayingDetailDTO struct {
Approval *approvalDTO.ApprovalBaseDTO `json:"approval,omitempty"` Approval *approvalDTO.ApprovalBaseDTO `json:"approval,omitempty"`
} }
// === Available Quantity DTOs ===
type KandangAvailableQtyDTO struct {
ProjectFlockKandangId uint `json:"project_flock_kandang_id"`
AvailableQty float64 `json:"available_qty"`
}
type AvailableQtyForTransferDTO struct {
ProjectFlockId uint `json:"project_flock_id"`
ProjectFlockCode string `json:"project_flock_code"`
Category string `json:"category"`
Kandangs []KandangAvailableQtyDTO `json:"kandangs"`
}
// === Mapper Functions === // === Mapper Functions ===
func ToProjectFlockSummaryDTO(pf *entity.ProjectFlock) *ProjectFlockSummaryDTO { func ToProjectFlockSummaryDTO(pf *entity.ProjectFlock) *ProjectFlockSummaryDTO {
@@ -207,7 +221,6 @@ func ToTransferLayingListDTO(e entity.LayingTransfer) TransferLayingListDTO {
func ToTransferLayingDetailDTO(e entity.LayingTransfer, approvals []entity.Approval) TransferLayingDetailDTO { func ToTransferLayingDetailDTO(e entity.LayingTransfer, approvals []entity.Approval) TransferLayingDetailDTO {
var latestApproval *approvalDTO.ApprovalBaseDTO var latestApproval *approvalDTO.ApprovalBaseDTO
// Use LatestApproval from entity if available
if e.LatestApproval != nil { if e.LatestApproval != nil {
mapped := approvalDTO.ToApprovalDTO(*e.LatestApproval) mapped := approvalDTO.ToApprovalDTO(*e.LatestApproval)
latestApproval = &mapped latestApproval = &mapped
@@ -27,4 +27,5 @@ func TransferLayingRoutes(v1 fiber.Router, u user.UserService, s transferLaying.
route.Patch("/:id", ctrl.UpdateOne) route.Patch("/:id", ctrl.UpdateOne)
route.Delete("/:id", ctrl.DeleteOne) route.Delete("/:id", ctrl.DeleteOne)
route.Post("/approvals", ctrl.Approval) route.Post("/approvals", ctrl.Approval)
route.Get("/project-flocks/:project_flock_id/available-qty", ctrl.GetAvailableQtyPerKandang)
} }
@@ -31,6 +31,7 @@ type TransferLayingService interface {
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.LayingTransfer, error) UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.LayingTransfer, error)
DeleteOne(ctx *fiber.Ctx, id uint) error DeleteOne(ctx *fiber.Ctx, id uint) error
Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.LayingTransfer, error) Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.LayingTransfer, error)
GetAvailableQtyPerKandang(ctx *fiber.Ctx, projectFlockID uint) (*entity.ProjectFlock, map[uint]float64, error)
} }
type transferLayingService struct { type transferLayingService struct {
@@ -92,32 +93,7 @@ func (s transferLayingService) GetAll(c *fiber.Ctx, params *validation.Query) ([
transferLayings, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { transferLayings, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db) db = s.withRelations(db)
if params.SourceProjectFlockId != 0 { db = db.Order("created_at DESC")
db = db.Where("from_project_flock_id = ?", params.SourceProjectFlockId)
}
if params.TargetProjectFlockId != 0 {
db = db.Where("to_project_flock_id = ?", params.TargetProjectFlockId)
}
if params.TransferDateFrom != "" {
db = db.Where("transfer_date >= ?", params.TransferDateFrom)
}
if params.TransferDateTo != "" {
db = db.Where("transfer_date <= ?", params.TransferDateTo)
}
if params.TransferNumber != "" {
db = db.Where("transfer_number ILIKE ?", "%"+params.TransferNumber+"%")
}
sortField := "created_at"
if params.Sort != "" {
sortField = params.Sort
}
sortOrder := "DESC"
if params.Order == "asc" {
sortOrder = "ASC"
}
db = db.Order(fmt.Sprintf("%s %s", sortField, sortOrder))
return db return db
}) })
@@ -126,19 +102,14 @@ func (s transferLayingService) GetAll(c *fiber.Ctx, params *validation.Query) ([
return nil, 0, err return nil, 0, err
} }
if params.ApprovalStatus != "" { approvalRepo := commonRepo.NewApprovalRepository(s.Repository.DB())
var filtered []entity.LayingTransfer for i, transfer := range transferLayings {
approvalRepo := commonRepo.NewApprovalRepository(s.Repository.DB()) latestApproval, err := approvalRepo.LatestByTarget(c.Context(), string(utils.ApprovalWorkflowTransferToLaying), transfer.Id, func(db *gorm.DB) *gorm.DB {
return db.Preload("ActionUser")
for _, transfer := range transferLayings { })
latestApproval, err := approvalRepo.LatestByTarget(c.Context(), string(utils.ApprovalWorkflowTransferToLaying), transfer.Id, nil) if err == nil && latestApproval != nil {
if err == nil && latestApproval != nil && latestApproval.Action != nil { transferLayings[i].LatestApproval = latestApproval
if string(*latestApproval.Action) == params.ApprovalStatus {
filtered = append(filtered, transfer)
}
}
} }
transferLayings = filtered
} }
return transferLayings, total, nil return transferLayings, total, nil
@@ -155,7 +126,9 @@ func (s transferLayingService) GetOne(c *fiber.Ctx, id uint) (*entity.LayingTran
} }
approvalRepo := commonRepo.NewApprovalRepository(s.Repository.DB()) approvalRepo := commonRepo.NewApprovalRepository(s.Repository.DB())
latestApproval, err := approvalRepo.LatestByTarget(c.Context(), string(utils.ApprovalWorkflowTransferToLaying), transferLaying.Id, nil) latestApproval, err := approvalRepo.LatestByTarget(c.Context(), string(utils.ApprovalWorkflowTransferToLaying), transferLaying.Id, func(db *gorm.DB) *gorm.DB {
return db.Preload("ActionUser")
})
if err == nil && latestApproval != nil { if err == nil && latestApproval != nil {
transferLaying.LatestApproval = latestApproval transferLaying.LatestApproval = latestApproval
} }
@@ -547,7 +520,6 @@ func (s transferLayingService) DeleteOne(c *fiber.Ctx, id uint) error {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Cannot delete transfer laying with status %s", action)) return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Cannot delete transfer laying with status %s", action))
} }
} }
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
productWarehouseRepoTx := s.ProductWarehouseRepo.WithTx(dbTransaction) productWarehouseRepoTx := s.ProductWarehouseRepo.WithTx(dbTransaction)
@@ -851,8 +823,6 @@ func (s *transferLayingService) restoreProjectFlockPopulation(ctx context.Contex
return fiber.NewError(fiber.StatusBadRequest, "No populations found for restoration") return fiber.NewError(fiber.StatusBadRequest, "No populations found for restoration")
} }
// Restore in LIFO order (from newest to oldest)
// Add all quantity back to the last (newest) population
if len(populations) > 0 { if len(populations) > 0 {
lastPop := populations[len(populations)-1] lastPop := populations[len(populations)-1]
newQty := lastPop.TotalQty + quantityToRestore newQty := lastPop.TotalQty + quantityToRestore
@@ -863,3 +833,37 @@ func (s *transferLayingService) restoreProjectFlockPopulation(ctx context.Contex
return nil return nil
} }
func (s transferLayingService) GetAvailableQtyPerKandang(ctx *fiber.Ctx, projectFlockID uint) (*entity.ProjectFlock, map[uint]float64, error) {
pf, err := s.ProjectFlockRepo.GetByID(ctx.Context(), projectFlockID, func(db *gorm.DB) *gorm.DB {
return db
})
if err != nil {
s.Log.Errorf("Failed to get project flock %d: %+v", projectFlockID, err)
return nil, nil, fiber.NewError(fiber.StatusNotFound, "Project flock not found")
}
kandangs, _, err := s.ProjectFlockKandangRepo.GetAll(ctx.Context(), 0, 1000, func(db *gorm.DB) *gorm.DB {
return db.Where("project_flock_id = ?", projectFlockID).Order("kandang_id ASC")
})
if err != nil {
s.Log.Errorf("Failed to get kandangs for project flock %d: %+v", projectFlockID, err)
return nil, nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch kandangs")
}
kandangAvailableQty := make(map[uint]float64)
for _, kandang := range kandangs {
totalQty, err := s.ProjectFlockPopulationRepo.GetTotalQtyByProjectFlockKandangID(ctx.Context(), kandang.Id)
if err != nil {
s.Log.Warnf("Failed to get total qty for kandang %d: %+v", kandang.Id, err)
kandangAvailableQty[kandang.Id] = 0
continue
}
kandangAvailableQty[kandang.Id] = totalQty
}
return pf, kandangAvailableQty, nil
}
@@ -29,16 +29,8 @@ type Update struct {
} }
type Query struct { type Query struct {
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"` Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"` Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
SourceProjectFlockId uint `query:"source_project_flock_id" validate:"omitempty"`
TargetProjectFlockId uint `query:"target_project_flock_id" validate:"omitempty"`
TransferDateFrom string `query:"transfer_date_from" validate:"omitempty,datetime=2006-01-02"`
TransferDateTo string `query:"transfer_date_to" validate:"omitempty,datetime=2006-01-02"`
ApprovalStatus string `query:"approval_status" validate:"omitempty,oneof=PENDING APPROVED REJECTED"` // Filter by latest approval status
TransferNumber string `query:"transfer_number" validate:"omitempty"` // Search by transfer number
Sort string `query:"sort" validate:"omitempty,oneof=created_at transfer_date"` // Sort by field
Order string `query:"order" validate:"omitempty,oneof=asc desc"` // Sort order
} }
type Approve struct { type Approve struct {
+2
View File
@@ -15,6 +15,7 @@ import (
production "gitlab.com/mbugroup/lti-api.git/internal/modules/production" production "gitlab.com/mbugroup/lti-api.git/internal/modules/production"
purchases "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases" purchases "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases"
users "gitlab.com/mbugroup/lti-api.git/internal/modules/users" users "gitlab.com/mbugroup/lti-api.git/internal/modules/users"
marketing "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing"
// MODULE IMPORTS // MODULE IMPORTS
) )
@@ -32,6 +33,7 @@ func Routes(app *fiber.App, db *gorm.DB) {
production.ProductionModule{}, production.ProductionModule{},
approvals.ApprovalModule{}, approvals.ApprovalModule{},
purchases.PurchaseModule{}, purchases.PurchaseModule{},
marketing.MarketingModule{},
// MODULE REGISTRY // MODULE REGISTRY
} }