feat[BE}: change get penjualan repport dto an add more params

This commit is contained in:
aguhh18
2025-12-16 21:10:48 +07:00
parent 062a7937e2
commit afe4b2ffe3
10 changed files with 741 additions and 247 deletions
@@ -62,16 +62,18 @@ func (c *RepportController) GetExpense(ctx *fiber.Ctx) error {
func (c *RepportController) GetMarketing(ctx *fiber.Ctx) error {
query := &validation.MarketingQuery{
Page: ctx.QueryInt("page", 1),
Limit: ctx.QueryInt("limit", 10),
Search: ctx.Query("search", ""),
CustomerId: int64(ctx.QueryInt("customer_id", 0)),
ProjectFlockKandangId: int64(ctx.QueryInt("project_flock_kandang_id", 0)),
DeliveryDate: ctx.Query("delivery_date", ""),
ProductId: int64(ctx.QueryInt("product_id", 0)),
WarehouseId: int64(ctx.QueryInt("warehouse_id", 0)),
SalesPersonId: int64(ctx.QueryInt("sales_person_id", 0)),
MarketingId: int64(ctx.QueryInt("marketing_id", 0)),
Page: ctx.QueryInt("page", 1),
Limit: ctx.QueryInt("limit", 10),
Search: ctx.Query("search", ""),
CustomerId: int64(ctx.QueryInt("customer_id", 0)),
ProductId: int64(ctx.QueryInt("product_id", 0)),
WarehouseId: int64(ctx.QueryInt("warehouse_id", 0)),
SalesPersonId: int64(ctx.QueryInt("sales_person_id", 0)),
FilterBy: ctx.Query("filter_by", ""),
StartDate: ctx.Query("start_date", ""),
EndDate: ctx.Query("end_date", ""),
SortBy: ctx.Query("sort_by", ""),
SortOrder: ctx.Query("sort_order", ""),
}
if query.Page < 1 || query.Limit < 1 {
@@ -84,7 +86,7 @@ func (c *RepportController) GetMarketing(ctx *fiber.Ctx) error {
}
return ctx.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.RepportMarketingListDTO]{
JSON(response.SuccessWithPaginate[dto.RepportMarketingItemDTO]{
Code: fiber.StatusOK,
Status: "success",
Message: "Get marketing report successfully",
@@ -1,219 +1,121 @@
package dto
import (
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto"
marketingDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/dto"
customerDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/dto"
productDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/dto"
warehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/dto"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
)
// === DTO Structs ===
// === Main Report Item DTO ===
type RepportMarketingBaseDTO struct {
Id uint `json:"id"`
SoNumber string `json:"so_number"`
SoDate time.Time `json:"so_date"`
Customer *customerDTO.CustomerRelationDTO `json:"customer,omitempty"`
SalesPerson *userDTO.UserRelationDTO `json:"sales_person,omitempty"`
Notes string `json:"notes"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
type RepportMarketingItemDTO struct {
DoDate string `json:"do_date"`
RealizationDate string `json:"realization_date"`
AgingDays int `json:"aging_days"`
Warehouse *warehouseDTO.WarehouseRelationDTO `json:"warehouse,omitempty"`
Customer *customerDTO.CustomerRelationDTO `json:"customer,omitempty"`
DoNumber string `json:"do_number"`
Sales *userDTO.UserRelationDTO `json:"sales,omitempty"`
VehicleNumber string `json:"vehicle_number"`
Product *productDTO.ProductRelationDTO `json:"product,omitempty"`
MarketingType string `json:"marketing_type"`
Qty float64 `json:"qty"`
AverageWeightKg float64 `json:"average_weight_kg"`
TotalWeightKg float64 `json:"total_weight_kg"`
SalesPricePerKg float64 `json:"sales_price_per_kg"`
HppPricePerKg float64 `json:"hpp_price_per_kg"`
SalesAmount float64 `json:"sales_amount"`
HppAmount float64 `json:"hpp_amount"`
}
type RepportMarketingProductDTO struct {
Id uint `json:"id"`
MarketingProductId uint `json:"marketing_product_id"`
Qty float64 `json:"qty"`
UnitPrice float64 `json:"unit_price"`
AvgWeight float64 `json:"avg_weight"`
TotalWeight float64 `json:"total_weight"`
TotalPrice float64 `json:"total_price"`
Product *productDTO.ProductRelationDTO `json:"product,omitempty"`
CreatedAt time.Time `json:"created_at"`
}
// === Report Response DTO ===
type RepportMarketingDeliveryDTO struct {
Id uint `json:"id"`
MarketingProductId uint `json:"marketing_product_id"`
Qty float64 `json:"qty"`
UnitPrice float64 `json:"unit_price"`
TotalWeight float64 `json:"total_weight"`
AvgWeight float64 `json:"avg_weight"`
TotalPrice float64 `json:"total_price"`
DeliveryDate *time.Time `json:"delivery_date,omitempty"`
VehicleNumber string `json:"vehicle_number"`
DoNumber string `json:"do_number"`
Product *productDTO.ProductRelationDTO `json:"product,omitempty"`
CreatedAt time.Time `json:"created_at"`
}
type RepportMarketingListDTO struct {
RepportMarketingBaseDTO
MarketingProduct RepportMarketingProductDTO `json:"marketing_product"`
MarketingDelivery RepportMarketingDeliveryDTO `json:"marketing_delivery"`
TotalMarketingProduct float64 `json:"total_marketing_product"`
TotalMarketingDelivery float64 `json:"total_marketing_delivery"`
LatestApproval *approvalDTO.ApprovalRelationDTO `json:"latest_approval,omitempty"`
type RepportMarketingResponseDTO struct {
Items []RepportMarketingItemDTO `json:"items"`
}
// === MAPPERS ===
func ToRepportMarketingBaseDTO(m *entity.Marketing) RepportMarketingBaseDTO {
if m == nil {
return RepportMarketingBaseDTO{}
// ToRepportMarketingItemDTO maps marketing delivery product to detailed report item
func ToRepportMarketingItemDTO(mdp entity.MarketingDeliveryProduct, hppPricePerKg float64) RepportMarketingItemDTO {
agingDays := 0
doDate := ""
if mdp.DeliveryDate != nil {
doDate = mdp.DeliveryDate.Format("02-Jan-2006")
}
var customer *customerDTO.CustomerRelationDTO
if m.Customer.Id != 0 {
mapped := customerDTO.ToCustomerRelationDTO(m.Customer)
customer = &mapped
realizationDate := ""
if mdp.DeliveryDate != nil {
realizationDate = mdp.DeliveryDate.Format("02-Jan-2006")
}
var salesPerson *userDTO.UserRelationDTO
if m.SalesPerson.Id != 0 {
mapped := userDTO.ToUserRelationDTO(m.SalesPerson)
salesPerson = &mapped
// Calculate sales_amount = total_weight_kg * sales_price_per_kg
salesAmount := mdp.TotalWeight * mdp.UnitPrice
// Calculate hpp_amount = total_weight_kg * hpp_price_per_kg
hppAmount := mdp.TotalWeight * hppPricePerKg
item := RepportMarketingItemDTO{
DoDate: doDate,
RealizationDate: realizationDate,
AgingDays: agingDays,
DoNumber: mdp.MarketingProduct.Marketing.SoNumber,
MarketingType: "ayam",
Qty: mdp.Qty,
AverageWeightKg: mdp.AvgWeight,
TotalWeightKg: mdp.TotalWeight,
SalesPricePerKg: mdp.UnitPrice,
HppPricePerKg: hppPricePerKg,
SalesAmount: salesAmount,
HppAmount: hppAmount,
}
return RepportMarketingBaseDTO{
Id: m.Id,
SoNumber: m.SoNumber,
SoDate: m.SoDate,
Customer: customer,
SalesPerson: salesPerson,
Notes: m.Notes,
CreatedAt: m.CreatedAt,
UpdatedAt: m.UpdatedAt,
}
}
func ToRepportMarketingProductDTO(mp *entity.MarketingProduct) RepportMarketingProductDTO {
if mp == nil {
return RepportMarketingProductDTO{}
// Map warehouse with full details
if mdp.MarketingProduct.ProductWarehouse.WarehouseId != 0 {
mapped := warehouseDTO.ToWarehouseRelationDTO(mdp.MarketingProduct.ProductWarehouse.Warehouse)
item.Warehouse = &mapped
}
var product *productDTO.ProductRelationDTO
if mp.ProductWarehouse.Product.Id != 0 {
mapped := productDTO.ToProductRelationDTO(mp.ProductWarehouse.Product)
product = &mapped
// Map customer using CustomerRelationDTO
if mdp.MarketingProduct.Marketing.CustomerId != 0 {
mapped := customerDTO.ToCustomerRelationDTO(mdp.MarketingProduct.Marketing.Customer)
item.Customer = &mapped
}
return RepportMarketingProductDTO{
Id: mp.Id,
MarketingProductId: mp.Id,
Qty: mp.Qty,
UnitPrice: mp.UnitPrice,
AvgWeight: mp.AvgWeight,
TotalWeight: mp.TotalWeight,
TotalPrice: mp.TotalPrice,
Product: product,
CreatedAt: time.Now(),
}
}
func ToRepportMarketingDeliveryDTO(mdp *entity.MarketingDeliveryProduct, soNumber string) RepportMarketingDeliveryDTO {
if mdp == nil {
return RepportMarketingDeliveryDTO{}
// Map sales person
if mdp.MarketingProduct.Marketing.SalesPersonId != 0 {
mapped := userDTO.ToUserRelationDTO(mdp.MarketingProduct.Marketing.SalesPerson)
item.Sales = &mapped
}
var product *productDTO.ProductRelationDTO
if mdp.MarketingProduct.ProductWarehouse.Product.Id != 0 {
// Map vehicle number
item.VehicleNumber = mdp.VehicleNumber
// Map product using ProductRelationDTO
if mdp.MarketingProduct.ProductWarehouse.ProductId != 0 {
mapped := productDTO.ToProductRelationDTO(mdp.MarketingProduct.ProductWarehouse.Product)
product = &mapped
item.Product = &mapped
}
warehouseId := uint(0)
if mdp.MarketingProduct.ProductWarehouse.Id != 0 {
warehouseId = mdp.MarketingProduct.ProductWarehouse.WarehouseId
}
doNumber := marketingDTO.GenerateDeliveryOrderNumber(soNumber, mdp.DeliveryDate, warehouseId)
return RepportMarketingDeliveryDTO{
Id: mdp.Id,
MarketingProductId: mdp.MarketingProductId,
Qty: mdp.Qty,
UnitPrice: mdp.UnitPrice,
TotalWeight: mdp.TotalWeight,
AvgWeight: mdp.AvgWeight,
TotalPrice: mdp.TotalPrice,
DeliveryDate: mdp.DeliveryDate,
VehicleNumber: mdp.VehicleNumber,
DoNumber: doNumber,
Product: product,
CreatedAt: time.Now(),
}
return item
}
func ToRepportMarketingListDTO(baseDTO RepportMarketingBaseDTO, mp *entity.MarketingProduct, mdp *entity.MarketingDeliveryProduct, latestApproval *approvalDTO.ApprovalRelationDTO) RepportMarketingListDTO {
var marketingProduct RepportMarketingProductDTO
var marketingDelivery RepportMarketingDeliveryDTO
if mp != nil {
marketingProduct = ToRepportMarketingProductDTO(mp)
}
if mdp != nil {
marketingDelivery = ToRepportMarketingDeliveryDTO(mdp, baseDTO.SoNumber)
}
totalMarketingProduct := float64(0)
totalMarketingDelivery := float64(0)
if mp != nil {
totalMarketingProduct = mp.Qty * mp.UnitPrice
}
if mdp != nil {
totalMarketingDelivery = mdp.Qty * mdp.UnitPrice
}
return RepportMarketingListDTO{
RepportMarketingBaseDTO: baseDTO,
MarketingProduct: marketingProduct,
MarketingDelivery: marketingDelivery,
TotalMarketingProduct: totalMarketingProduct,
TotalMarketingDelivery: totalMarketingDelivery,
LatestApproval: latestApproval,
// ToRepportMarketingItemDTOs maps array of delivery products to report items with HPP calculation
func ToRepportMarketingItemDTOs(mdps []entity.MarketingDeliveryProduct, hppPricePerKg float64) []RepportMarketingItemDTO {
items := make([]RepportMarketingItemDTO, 0, len(mdps))
for _, mdp := range mdps {
items = append(items, ToRepportMarketingItemDTO(mdp, hppPricePerKg))
}
return items
}
func ToRepportMarketingListDTOs(deliveryProducts []entity.MarketingDeliveryProduct) []RepportMarketingListDTO {
result := make([]RepportMarketingListDTO, 0, len(deliveryProducts))
// ToRepportMarketingResponseDTO creates complete marketing report response
func ToRepportMarketingResponseDTO(mdps []entity.MarketingDeliveryProduct, hppPricePerKg float64) RepportMarketingResponseDTO {
items := ToRepportMarketingItemDTOs(mdps, hppPricePerKg)
marketingMap := make(map[uint]entity.MarketingDeliveryProduct)
for _, dp := range deliveryProducts {
if dp.MarketingProduct.Marketing.Id == 0 {
continue
}
marketingID := dp.MarketingProduct.Marketing.Id
if _, exists := marketingMap[marketingID]; !exists {
marketingMap[marketingID] = dp
}
return RepportMarketingResponseDTO{
Items: items,
}
for _, deliveryProduct := range marketingMap {
if deliveryProduct.MarketingProduct.Marketing.Id == 0 {
continue
}
marketing := &deliveryProduct.MarketingProduct.Marketing
baseDTO := ToRepportMarketingBaseDTO(marketing)
var latestApproval *approvalDTO.ApprovalRelationDTO
if marketing.LatestApproval != nil {
mapped := approvalDTO.ToApprovalDTO(*marketing.LatestApproval)
latestApproval = &mapped
}
mdp := &deliveryProduct
dto := ToRepportMarketingListDTO(baseDTO, &deliveryProduct.MarketingProduct, mdp, latestApproval)
result = append(result, dto)
}
return result
}
@@ -1,6 +1,9 @@
package service
import (
"context"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gitlab.com/mbugroup/lti-api.git/internal/modules/repports/dto"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/validations"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
@@ -18,7 +21,7 @@ import (
type RepportService interface {
GetExpense(ctx *fiber.Ctx, params *validation.ExpenseQuery) ([]dto.RepportExpenseListDTO, int64, error)
GetMarketing(ctx *fiber.Ctx, params *validation.MarketingQuery) ([]dto.RepportMarketingListDTO, int64, error)
GetMarketing(ctx *fiber.Ctx, params *validation.MarketingQuery) ([]dto.RepportMarketingItemDTO, int64, error)
}
type repportService struct {
@@ -77,7 +80,7 @@ func (s *repportService) GetExpense(c *fiber.Ctx, params *validation.ExpenseQuer
return result, total, nil
}
func (s *repportService) GetMarketing(c *fiber.Ctx, params *validation.MarketingQuery) ([]dto.RepportMarketingListDTO, int64, error) {
func (s *repportService) GetMarketing(c *fiber.Ctx, params *validation.MarketingQuery) ([]dto.RepportMarketingItemDTO, int64, error) {
if err := s.Validate.Struct(params); err != nil {
return nil, 0, err
}
@@ -89,27 +92,88 @@ func (s *repportService) GetMarketing(c *fiber.Ctx, params *validation.Marketing
return nil, 0, err
}
marketingIDMap := make(map[uint]bool)
marketingIDs := make([]uint, 0)
for _, dp := range deliveryProducts {
if marketingID := dp.MarketingProduct.Marketing.Id; marketingID > 0 && !marketingIDMap[marketingID] {
marketingIDs = append(marketingIDs, marketingID)
marketingIDMap[marketingID] = true
}
}
projectFlockIDs := s.collectProjectFlockIDs(deliveryProducts)
hppMap := s.buildHppMap(c.Context(), projectFlockIDs, deliveryProducts)
items := s.mapDeliveryProductsToDTOs(deliveryProducts, hppMap)
approvals, err := s.ApprovalSvc.LatestByTargets(c.Context(), utils.ApprovalWorkflowMarketing, marketingIDs, func(db *gorm.DB) *gorm.DB {
return db.Preload("ActionUser")
})
if err != nil {
s.Log.Warnf("LatestByTargets error: %v", err)
}
for i := range deliveryProducts {
if approval, exists := approvals[deliveryProducts[i].MarketingProduct.Marketing.Id]; exists && approval != nil {
deliveryProducts[i].MarketingProduct.Marketing.LatestApproval = approval
}
}
return dto.ToRepportMarketingListDTOs(deliveryProducts), total, nil
return items, total, nil
}
func (s *repportService) collectProjectFlockIDs(deliveryProducts []entity.MarketingDeliveryProduct) []uint {
projectFlockIDMap := make(map[uint]bool)
projectFlockIDs := make([]uint, 0)
for _, dp := range deliveryProducts {
if projectFlockKandang := dp.MarketingProduct.ProductWarehouse.ProjectFlockKandang; projectFlockKandang != nil {
if projectFlockKandang.ProjectFlockId > 0 && !projectFlockIDMap[projectFlockKandang.ProjectFlockId] {
projectFlockIDs = append(projectFlockIDs, projectFlockKandang.ProjectFlockId)
projectFlockIDMap[projectFlockKandang.ProjectFlockId] = true
}
}
}
return projectFlockIDs
}
func (s *repportService) buildHppMap(ctx context.Context, projectFlockIDs []uint, deliveryProducts []entity.MarketingDeliveryProduct) map[uint]float64 {
hppMap := make(map[uint]float64)
for _, projectFlockID := range projectFlockIDs {
hppPerKg := s.calculateHppPricePerKg(ctx, projectFlockID, deliveryProducts)
hppMap[projectFlockID] = hppPerKg
}
return hppMap
}
func (s *repportService) mapDeliveryProductsToDTOs(deliveryProducts []entity.MarketingDeliveryProduct, hppMap map[uint]float64) []dto.RepportMarketingItemDTO {
items := make([]dto.RepportMarketingItemDTO, 0, len(deliveryProducts))
for _, dp := range deliveryProducts {
hppPerKg := float64(0)
if projectFlockKandang := dp.MarketingProduct.ProductWarehouse.ProjectFlockKandang; projectFlockKandang != nil {
if hpp, exists := hppMap[projectFlockKandang.ProjectFlockId]; exists {
hppPerKg = hpp
}
}
items = append(items, dto.ToRepportMarketingItemDTO(dp, hppPerKg))
}
return items
}
func (s *repportService) calculateHppPricePerKg(ctx context.Context, projectFlockID uint, deliveryProducts []entity.MarketingDeliveryProduct) float64 {
if projectFlockID == 0 {
return 0
}
realizations, err := s.ExpenseRealizationRepo.GetByProjectFlockID(ctx, projectFlockID)
if err != nil {
return 0
}
if len(realizations) == 0 {
return 0
}
totalActualCost := float64(0)
for _, realization := range realizations {
cost := realization.Price * realization.Qty
totalActualCost += cost
}
if totalActualCost == 0 {
return 0
}
totalWeightSold := float64(0)
for _, dp := range deliveryProducts {
if dp.MarketingProduct.ProductWarehouse.ProjectFlockKandang != nil &&
dp.MarketingProduct.ProductWarehouse.ProjectFlockKandang.ProjectFlockId == projectFlockID {
totalWeightSold += dp.TotalWeight
}
}
if totalWeightSold == 0 {
return 0
}
hppPerKg := totalActualCost / totalWeightSold
return hppPerKg
}
@@ -16,14 +16,16 @@ type ExpenseQuery struct {
}
type MarketingQuery struct {
Page int `query:"page" validate:"omitempty,min=1,gt=0"`
Limit int `query:"limit" validate:"omitempty,min=1,max=100,gt=0"`
Search string `query:"search" validate:"omitempty,max=100"`
CustomerId int64 `query:"customer_id" validate:"omitempty"`
ProjectFlockKandangId int64 `query:"project_flock_kandang_id" validate:"omitempty"`
DeliveryDate string `query:"delivery_date" validate:"omitempty"`
ProductId int64 `query:"product_id" validate:"omitempty"`
WarehouseId int64 `query:"warehouse_id" validate:"omitempty"`
SalesPersonId int64 `query:"sales_person_id" validate:"omitempty"`
MarketingId int64 `query:"marketing_id" validate:"omitempty"`
Page int `query:"page" validate:"omitempty,min=1,gt=0"`
Limit int `query:"limit" validate:"omitempty,min=1,max=100,gt=0"`
Search string `query:"search" validate:"omitempty,max=100"`
CustomerId int64 `query:"customer_id" validate:"omitempty"`
ProductId int64 `query:"product_id" validate:"omitempty"`
WarehouseId int64 `query:"warehouse_id" validate:"omitempty"`
SalesPersonId int64 `query:"sales_person_id" validate:"omitempty"`
FilterBy string `query:"filter_by" validate:"omitempty,oneof=realization_date delivery_date"`
StartDate string `query:"start_date" validate:"omitempty,datetime=2006-01-02"`
EndDate string `query:"end_date" validate:"omitempty,datetime=2006-01-02"`
SortBy string `query:"sort_by" validate:"omitempty,oneof=delivery_date customer warehouse product sales_person vehicle_number sales_amount hpp_amount qty average_weight total_weight sales_price hpp_price aging_days"`
SortOrder string `query:"sort_order" validate:"omitempty,oneof=asc desc"`
}