diff --git a/internal/modules/closings/dto/closingMarketing.dto.go b/internal/modules/closings/dto/closingMarketing.dto.go index a442fc9d..ea0ddb81 100644 --- a/internal/modules/closings/dto/closingMarketing.dto.go +++ b/internal/modules/closings/dto/closingMarketing.dto.go @@ -28,10 +28,7 @@ type SalesDTO struct { } type PenjualanRealisasiResponseDTO struct { - ProjectType string `json:"project_type"` - FlockId uint `json:"flock_id"` - Period int `json:"period"` - Sales []SalesDTO `json:"sales"` + Sales []SalesDTO `json:"sales"` } // === Mapper Functions === @@ -87,12 +84,10 @@ func ToSalesDTOs(e []entity.MarketingDeliveryProduct) []SalesDTO { } func ToPenjualanRealisasiResponseDTO(projectType string, projectFlockID uint, e []entity.MarketingDeliveryProduct) PenjualanRealisasiResponseDTO { - period := extractPeriodFromRealisasi(e) + return PenjualanRealisasiResponseDTO{ - ProjectType: projectType, - FlockId: projectFlockID, - Period: period, - Sales: ToSalesDTOs(e), + + Sales: ToSalesDTOs(e), } } diff --git a/internal/modules/expenses/repositories/expense_realization.repository.go b/internal/modules/expenses/repositories/expense_realization.repository.go index e60324ca..e4d57b79 100644 --- a/internal/modules/expenses/repositories/expense_realization.repository.go +++ b/internal/modules/expenses/repositories/expense_realization.repository.go @@ -5,6 +5,8 @@ import ( "gitlab.com/mbugroup/lti-api.git/internal/common/repository" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + validation "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/validations" + "gitlab.com/mbugroup/lti-api.git/internal/utils" "gorm.io/gorm" ) @@ -13,6 +15,7 @@ type ExpenseRealizationRepository interface { IdExists(ctx context.Context, id uint64) (bool, error) GetByExpenseNonstockID(ctx context.Context, expenseNonstockID uint64) (*entity.ExpenseRealization, error) GetByProjectFlockID(ctx context.Context, projectFlockID uint) ([]entity.ExpenseRealization, error) + GetAllWithFilters(ctx context.Context, offset, limit int, filters *validation.ExpenseQuery) ([]entity.ExpenseRealization, int64, error) } type ExpenseRealizationRepositoryImpl struct { @@ -50,3 +53,83 @@ func (r *ExpenseRealizationRepositoryImpl) GetByProjectFlockID(ctx context.Conte Find(&realizations).Error return realizations, err } + +func (r *ExpenseRealizationRepositoryImpl) GetAllWithFilters(ctx context.Context, offset, limit int, filters *validation.ExpenseQuery) ([]entity.ExpenseRealization, int64, error) { + var realizations []entity.ExpenseRealization + var total int64 + + db := r.DB().WithContext(ctx). + Model(&entity.ExpenseRealization{}). + Preload("ExpenseNonstock", func(db *gorm.DB) *gorm.DB { + return db. + Preload("Expense"). + Preload("Expense.Supplier"). + Preload("Kandang"). + Preload("Kandang.Location"). + Preload("Nonstock") + }). + Joins("JOIN expense_nonstocks ON expense_nonstocks.id = expense_realizations.expense_nonstock_id"). + Joins("JOIN expenses ON expenses.id = expense_nonstocks.expense_id"). + Joins("LEFT JOIN suppliers ON suppliers.id = expenses.supplier_id") + + if filters.Search != "" { + db = db.Where("expenses.category LIKE ? OR expenses.reference_number LIKE ? OR expenses.po_number LIKE ? OR expenses.notes LIKE ? OR suppliers.name LIKE ?", + "%"+filters.Search+"%", "%"+filters.Search+"%", "%"+filters.Search+"%", "%"+filters.Search+"%", "%"+filters.Search+"%") + } + + if filters.Category != "" { + db = db.Where("expenses.category = ?", filters.Category) + } + + if filters.SupplierId > 0 { + db = db.Where("expenses.supplier_id = ?", filters.SupplierId) + } + + if filters.KandangId > 0 { + db = db.Where("expense_nonstocks.kandang_id = ?", filters.KandangId) + } + + if filters.ProjectFlockKandangId > 0 { + db = db.Where("expense_nonstocks.project_flock_kandang_id = ?", filters.ProjectFlockKandangId) + } + + if filters.NonstockId > 0 { + db = db.Where("expense_nonstocks.nonstock_id = ?", filters.NonstockId) + } + + locationID := filters.LocationId + areaID := filters.AreaId + + if locationID > 0 || areaID > 0 { + db = db.Joins("JOIN kandangs ON kandangs.id = expense_nonstocks.kandang_id") + + if locationID > 0 { + db = db.Where("kandangs.location_id = ?", uint(locationID)) + } + + if areaID > 0 { + db = db.Joins("JOIN locations ON locations.id = kandangs.location_id"). + Where("locations.area_id = ?", uint(areaID)) + } + } + + if filters.RealizationDate != "" { + if realizationDate, err := utils.ParseDateString(filters.RealizationDate); err == nil { + db = db.Where("DATE(expenses.realization_date) = ?", realizationDate) + } + } + + if err := db.Count(&total).Error; err != nil { + return nil, 0, err + } + + if err := db. + Offset(offset). + Limit(limit). + Order("expense_realizations.created_at DESC"). + Find(&realizations).Error; err != nil { + return nil, 0, err + } + + return realizations, total, nil +} diff --git a/internal/modules/inventory/adjustments/dto/adjustment.dto.go b/internal/modules/inventory/adjustments/dto/adjustment.dto.go index 556050f4..008f9966 100644 --- a/internal/modules/inventory/adjustments/dto/adjustment.dto.go +++ b/internal/modules/inventory/adjustments/dto/adjustment.dto.go @@ -33,10 +33,8 @@ type ProductWarehouseDTO struct { type AdjustmentRelationDTO struct { Id uint `json:"id"` - TransactionType string `json:"transaction_type"` - Quantity float64 `json:"quantity"` - BeforeQuantity float64 `json:"before_quantity"` - AfterQuantity float64 `json:"after_quantity"` + Increase float64 `json:"increase"` + Decrease float64 `json:"decrease"` Note string `json:"note,omitempty"` ProductWarehouseId uint `json:"product_warehouse_id"` ProductWarehouse *ProductWarehouseDTO `json:"product_warehouse,omitempty"` @@ -104,12 +102,10 @@ func ToProductWarehouseDTO(e *entity.ProductWarehouse) *ProductWarehouseDTO { func ToAdjustmentRelationDTO(e *entity.StockLog) AdjustmentRelationDTO { return AdjustmentRelationDTO{ - Id: e.Id, - // TransactionType: e.LoggableType, - // Quantity: e.Q, - // BeforeQuantity: e.BeforeQuantity, - // AfterQuantity: e.AfterQuantity, + Id: e.Id, Note: e.Notes, + Increase: e.Increase, + Decrease: e.Decrease, ProductWarehouseId: e.ProductWarehouseId, ProductWarehouse: ToProductWarehouseDTO(e.ProductWarehouse), } diff --git a/internal/modules/inventory/adjustments/route.go b/internal/modules/inventory/adjustments/route.go index 57200215..8c7e5f9f 100644 --- a/internal/modules/inventory/adjustments/route.go +++ b/internal/modules/inventory/adjustments/route.go @@ -14,9 +14,9 @@ func AdjustmentRoutes(v1 fiber.Router, u user.UserService, s adjustment.Adjustme route := v1.Group("/adjustments") route.Use(m.Auth(u)) - // Standard CRUD routes following master data pattern - route.Get("/", ctrl.AdjustmentHistory) // Get all with pagination and filters - route.Post("/", ctrl.Adjustment) // Create adjustment + + route.Get("/", ctrl.AdjustmentHistory) + route.Post("/", ctrl.Adjustment) route.Get("/:id", ctrl.GetOne) } diff --git a/internal/modules/inventory/adjustments/services/adjustment.service.go b/internal/modules/inventory/adjustments/services/adjustment.service.go index da118438..7bcbca7e 100644 --- a/internal/modules/inventory/adjustments/services/adjustment.service.go +++ b/internal/modules/inventory/adjustments/services/adjustment.service.go @@ -141,7 +141,7 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e if err := common.EnsureProjectFlockNotClosedForProductWarehouses( ctx, s.StockLogsRepository.DB(), - []uint{pw.Id}, + []uint{pw.Id}, ); err != nil { return nil, err } @@ -229,7 +229,6 @@ func (s *adjustmentService) AdjustmentHistory(c *fiber.Ctx, query *validation.Qu isWarehousesExist, err := s.WarehouseRepo.IdExists(c.Context(), uint(query.WarehouseID)) if err != nil { - s.Log.Errorf("Failed to check warehouse existence: %+v", err) return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate warehouse") } if query.WarehouseID > 0 && !isWarehousesExist { diff --git a/internal/modules/inventory/product-warehouses/repositories/product_warehouse.repository.go b/internal/modules/inventory/product-warehouses/repositories/product_warehouse.repository.go index 8b33a852..2fc8bc3d 100644 --- a/internal/modules/inventory/product-warehouses/repositories/product_warehouse.repository.go +++ b/internal/modules/inventory/product-warehouses/repositories/product_warehouse.repository.go @@ -93,7 +93,7 @@ func (r *ProductWarehouseRepositoryImpl) GetByCategoryCodeAndWarehouseID(ctx con Joins("JOIN products ON products.id = product_warehouses.product_id"). Joins("JOIN product_categories ON product_categories.id = products.product_category_id"). Where("product_categories.code = ? AND product_warehouses.warehouse_id = ?", categoryCode, warehouseId). - Order("product_warehouses.created_at DESC") + Order("product_warehouses.id DESC") // preload relations so nested Product and Warehouse are populated err := q.Preload("Product").Preload("Warehouse").Find(&productWarehouses).Error diff --git a/internal/modules/inventory/transfers/services/transfer.service.go b/internal/modules/inventory/transfers/services/transfer.service.go index 3293d21b..f94295f6 100644 --- a/internal/modules/inventory/transfers/services/transfer.service.go +++ b/internal/modules/inventory/transfers/services/transfer.service.go @@ -59,6 +59,7 @@ func NewTransferService(validate *validator.Validate, stockTransferRepo rStockTr ProjectFlockKandangRepo: projectFlockKandangRepo, } } + func (s transferService) withRelations(db *gorm.DB) *gorm.DB { return db. Preload("CreatedUser"). @@ -96,13 +97,11 @@ func (s transferService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entit s.Log.Infof("Retrieved %d transfers", len(transfers)) return transfers, total, nil - } func (s transferService) GetOne(c *fiber.Ctx, id uint) (*entity.StockTransfer, error) { var transfer entity.StockTransfer - // gunakan repo secara langsung transferPtr, err := s.StockTransferRepo.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB { return s.withRelations(db) }) @@ -120,10 +119,9 @@ func (s transferService) GetOne(c *fiber.Ctx, id uint) (*entity.StockTransfer, e } func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferRequest) (*entity.StockTransfer, error) { - // Validasi stok di gudang asal harus exist dan mencukupi + pwIDs := make([]uint, 0, len(req.Products)) - // Validasi stok di gudang asal harus exist dan mencukupi for _, product := range req.Products { sourcePW, err := s.ProductWarehouseRepo.GetProductWarehouseByProductAndWarehouseID( c.Context(), uint(product.ProductID), uint(req.SourceWarehouseID), @@ -139,6 +137,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques } pwIDs = append(pwIDs, sourcePW.Id) } + if err := commonSvc.EnsureProjectFlockNotClosedForProductWarehouses( c.Context(), s.StockTransferRepo.DB(), @@ -152,7 +151,6 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques return nil, err } - // validasi total qty harus lebih besar dari atau sama dengan total qty di delivery compare berdasarkan productid deliveryQtyMap := make(map[uint]float64) for _, delivery := range req.Deliveries { for _, prod := range delivery.Products { @@ -160,7 +158,6 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques } } - // Cek: qty delivery tidak boleh melebihi qty di root for _, product := range req.Products { if deliveryQtyMap[product.ProductID] > product.ProductQty { return nil, fiber.NewError(fiber.StatusBadRequest, @@ -168,7 +165,6 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques } } - // cek suplier id caegory BOP cek by id for _, delivery := range req.Deliveries { supplier, err := s.SupplierRepo.GetByID(c.Context(), uint(delivery.SupplierID), nil) if err != nil { @@ -182,8 +178,6 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques } } - // Generate movement number - // Format: PND-MBU-00001 seqNum, err := s.StockTransferRepo.GetNextMovementNumber(c.Context()) if err != nil { s.Log.Errorf("Failed to get next movement number: %+v", err) @@ -201,17 +195,14 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques CreatedBy: uint64(actorID), } - // Save the transfer entity to the database err = s.StockTransferRepo.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error { - // Insert header if err := s.StockTransferRepo.WithTx(tx).CreateOne(c.Context(), entityTransfer, nil); err != nil { s.Log.Errorf("Failed to create stock transfer: %+v", err) return err } s.Log.Infof("Stock transfer created: %+v", entityTransfer.Id) - // insert ke details var details []*entity.StockTransferDetail for _, product := range req.Products { details = append(details, &entity.StockTransferDetail{ @@ -226,7 +217,6 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques } s.Log.Infof("Stock transfer details created for transfer ID: %+v", entityTransfer.Id) - // Tambahkan proses insert delivery var deliveries []*entity.StockTransferDelivery for _, delivery := range req.Deliveries { deliveries = append(deliveries, &entity.StockTransferDelivery{ @@ -234,7 +224,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques SupplierId: uint64(delivery.SupplierID), VehiclePlate: delivery.VehiclePlate, DriverName: delivery.DriverName, - DocumentPath: "https://tourism.gov.in/sites/default/files/2019-04/dummy-pdf_2.pdf", // todo: tunggu ada aws baru proses + DocumentPath: "https://tourism.gov.in/sites/default/files/2019-04/dummy-pdf_2.pdf", ShippingCostItem: delivery.DeliveryCostPerItem, ShippingCostTotal: delivery.DeliveryCost, }) @@ -243,7 +233,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques s.Log.Errorf("Failed to create stock transfer deliveries: %+v", err) return err } - // tambahkan insert ke delivery items sebagai pivot + detailMap := make(map[uint64]uint64) for _, d := range details { detailMap[d.ProductId] = d.Id @@ -271,9 +261,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques } s.Log.Infof("Stock transfer delivery items created for transfer ID: %+v", entityTransfer.Id) - // Proses pengurangan stok di gudang asal dan penambahan stok di gudang tujuan for _, product := range req.Products { - // Kurangi stok di gudang asal sourcePW, err := s.ProductWarehouseRepo.GetProductWarehouseByProductAndWarehouseID(c.Context(), uint(product.ProductID), uint(req.SourceWarehouseID)) if err != nil { s.Log.Errorf("Failed to get source product warehouse: %+v", err) @@ -290,15 +278,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques } s.Log.Infof("Source product warehouse updated: %+v", sourcePW.Id) - // create stock log for decrease (source) - // beforeQty := sourcePW.Quantity + product.ProductQty // sourcePW already decreased decreaseLog := &entity.StockLog{ - // TransactionType: entity.TransactionTypeDecrease, - // Quantity: product.ProductQty, - // BeforeQuantity: beforeQty, - // AfterQuantity: sourcePW.Qty, - // LogType: entity.LogTypeTransfer, - // LogId: uint(entityTransfer.Id), Decrease: product.ProductQty, Notes: "", LoggableType: entity.LogTypeTransfer, @@ -311,7 +291,6 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques return err } - // Tambah stok di gudang tujuan destPW, err := s.ProductWarehouseRepo.GetProductWarehouseByProductAndWarehouseID( c.Context(), uint(product.ProductID), uint(req.DestinationWarehouseID), ) @@ -320,7 +299,6 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques return fiber.NewError(fiber.StatusInternalServerError, "Failed to get destination product warehouse") } if err != nil && errors.Is(err, gorm.ErrRecordNotFound) { - // Jika belum ada record untuk produk di gudang tujuan, buat baru ctx := c.Context() projectFlockKandangID, err := s.getActiveProjectFlockKandangID(ctx, uint(req.DestinationWarehouseID)) if err != nil { @@ -331,7 +309,6 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques WarehouseId: uint(req.DestinationWarehouseID), Quantity: 0, ProjectFlockKandangId: &projectFlockKandangID, - // CreatedBy: 1, // TODO: should Get from auth middleware } if err := s.ProductWarehouseRepo.WithTx(tx).CreateOne(c.Context(), destPW, nil); err != nil { s.Log.Errorf("Failed to create destination product warehouse: %+v", err) @@ -339,7 +316,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques } s.Log.Infof("Destination product warehouse created: %+v", destPW.Id) } - // Update stok di gudang tujuan + destPW.Quantity += product.ProductQty if err := s.ProductWarehouseRepo.WithTx(tx).UpdateOne(c.Context(), destPW.Id, destPW, nil); err != nil { s.Log.Errorf("Failed to update destination product warehouse: %+v", err) @@ -347,13 +324,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques } s.Log.Infof("Destination product warehouse updated: %+v", destPW.Id) - // create stock log for increase (destination) - // beforeDestQty := destPW.Quantity - product.ProductQty increaseLog := &entity.StockLog{ - // TransactionType: entity.TransactionTypeIncrease, - // Quantity: product.ProductQty, - // BeforeQuantity: beforeDestQty, - // AfterQuantity: destPW.Qty, Increase: product.ProductQty, LoggableType: entity.LogTypeTransfer, LoggableId: uint(entityTransfer.Id), @@ -365,7 +336,6 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques s.Log.Errorf("Failed to create stock log increase: %+v", err) return err } - } return nil @@ -376,7 +346,6 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to process transfer transaction") } - // Ambil data lengkap hasil create dengan GetOne (agar preload relasi sama dengan GetOne) result, err := s.GetOne(c, uint(entityTransfer.Id)) if err != nil { return nil, err diff --git a/internal/modules/marketing/repositories/salesorder_delivery_product.repository.go b/internal/modules/marketing/repositories/salesorder_delivery_product.repository.go index a3c2af88..85d850a6 100644 --- a/internal/modules/marketing/repositories/salesorder_delivery_product.repository.go +++ b/internal/modules/marketing/repositories/salesorder_delivery_product.repository.go @@ -5,6 +5,8 @@ import ( "gitlab.com/mbugroup/lti-api.git/internal/common/repository" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + validation "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/validations" + "gitlab.com/mbugroup/lti-api.git/internal/utils" "gorm.io/gorm" ) @@ -13,6 +15,7 @@ type MarketingDeliveryProductRepository interface { GetDeliveryProductsByProjectFlockID(ctx context.Context, projectFlockID uint, callback func(*gorm.DB) *gorm.DB) ([]entity.MarketingDeliveryProduct, error) GetByMarketingId(ctx context.Context, marketingId uint) ([]entity.MarketingDeliveryProduct, error) GetByMarketingProductID(ctx context.Context, marketingProductID uint) (*entity.MarketingDeliveryProduct, error) + GetAllWithFilters(ctx context.Context, offset, limit int, filters *validation.MarketingQuery) ([]entity.MarketingDeliveryProduct, int64, error) } type MarketingDeliveryProductRepositoryImpl struct { @@ -74,3 +77,84 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetByMarketingProductID(ctx con return &deliveryProduct, nil } + +func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.Context, offset, limit int, filters *validation.MarketingQuery) ([]entity.MarketingDeliveryProduct, int64, error) { + var deliveryProducts []entity.MarketingDeliveryProduct + var total int64 + + db := r.DB().WithContext(ctx). + Model(&entity.MarketingDeliveryProduct{}). + Preload("MarketingProduct", func(db *gorm.DB) *gorm.DB { + return db. + Preload("Marketing"). + Preload("Marketing.Customer"). + Preload("Marketing.SalesPerson"). + Preload("ProductWarehouse"). + Preload("ProductWarehouse.Product"). + Preload("ProductWarehouse.Warehouse") + }). + Joins("JOIN marketing_products ON marketing_products.id = marketing_delivery_products.marketing_product_id"). + Joins("JOIN marketings ON marketings.id = marketing_products.marketing_id") + + if filters.ProductId > 0 || filters.WarehouseId > 0 || filters.ProjectFlockKandangId > 0 { + db = db.Joins("LEFT JOIN product_warehouses ON product_warehouses.id = marketing_products.product_warehouse_id") + } + + if filters.ProductId > 0 { + db = db.Joins("LEFT JOIN products ON products.id = product_warehouses.product_id") + } + + if filters.WarehouseId > 0 { + db = db.Joins("LEFT JOIN warehouses ON warehouses.id = product_warehouses.warehouse_id") + } + + if filters.Search != "" { + db = db.Where("marketing_delivery_products.vehicle_number ILIKE ?", + "%"+filters.Search+"%") + } + + if filters.CustomerId > 0 { + db = db.Where("marketings.customer_id = ?", filters.CustomerId) + } + + if filters.SalesPersonId > 0 { + db = db.Where("marketings.sales_person_id = ?", filters.SalesPersonId) + } + + if filters.MarketingId > 0 { + db = db.Where("marketings.id = ?", filters.MarketingId) + } + + if filters.ProductId > 0 { + db = db.Where("product_warehouses.product_id = ?", filters.ProductId) + } + + if filters.WarehouseId > 0 { + db = db.Where("product_warehouses.warehouse_id = ?", filters.WarehouseId) + } + + if filters.ProjectFlockKandangId > 0 { + db = db.Where("product_warehouses.project_flock_kandang_id = ?", filters.ProjectFlockKandangId) + } + + if filters.DeliveryDate != "" { + if deliveryDate, err := utils.ParseDateString(filters.DeliveryDate); err == nil { + nextDate := deliveryDate.AddDate(0, 0, 1) + db = db.Where("marketing_delivery_products.delivery_date >= ? AND marketing_delivery_products.delivery_date < ?", deliveryDate, nextDate) + } + } + + if err := db.Count(&total).Error; err != nil { + return nil, 0, err + } + + if err := db. + Offset(offset). + Limit(limit). + Order("marketing_delivery_products.id DESC"). + Find(&deliveryProducts).Error; err != nil { + return nil, 0, err + } + + return deliveryProducts, total, nil +} diff --git a/internal/modules/production/project_flocks/controllers/projectflock.controller.go b/internal/modules/production/project_flocks/controllers/projectflock.controller.go index 937c9058..c48e1e2a 100644 --- a/internal/modules/production/project_flocks/controllers/projectflock.controller.go +++ b/internal/modules/production/project_flocks/controllers/projectflock.controller.go @@ -281,7 +281,6 @@ func (u *ProjectflockController) LookupProjectFlockKandang(c *fiber.Ctx) error { dtoResult := dto.ToProjectFlockKandangDTO(*result) dtoResult.AvailableQuantity = float64(availableStock) - // populate available quantity for each kandang inside project_flock if dtoResult.ProjectFlock != nil { for i := range dtoResult.ProjectFlock.Kandangs { kand := &dtoResult.ProjectFlock.Kandangs[i] @@ -292,7 +291,7 @@ func (u *ProjectflockController) LookupProjectFlockKandang(c *fiber.Ctx) error { kand.AvailableQuantity = q } } - // remove inner kandangs from project_flock to avoid duplication + dtoResult.ProjectFlock.Kandangs = nil } 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 889a95be..2d532335 100644 --- a/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go +++ b/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go @@ -117,10 +117,10 @@ func (r *projectFlockKandangRepositoryImpl) GetAllWithFilters(ctx context.Contex AND "approvals"."approvable_type" = ? AND LOWER("approvals"."step_name") = LOWER(?) AND "approvals"."id" IN ( - SELECT "id" FROM "approvals" - WHERE "approvable_id" = "project_flock_kandangs"."id" - AND "approvable_type" = ? - ORDER BY "action_at" DESC + SELECT "approvals"."id" FROM "approvals" + WHERE "approvals"."approvable_id" = "project_flock_kandangs"."id" + AND "approvals"."approvable_type" = ? + ORDER BY "approvals"."id" DESC LIMIT 1 ) ) @@ -238,9 +238,9 @@ func (r *projectFlockKandangRepositoryImpl) GetByProjectFlockAndKandang(ctx cont func (r *projectFlockKandangRepositoryImpl) GetActiveByKandangID(ctx context.Context, kandangID uint) (*entity.ProjectFlockKandang, error) { latestApprovalSubQuery := r.db. Table("approvals"). - Select("DISTINCT ON (approvable_id) approvable_id, step_name, action_at"). + Select("DISTINCT ON (approvable_id) approvable_id, step_name, id"). Where("approvable_type = ?", "PROJECT_FLOCKS"). - Order("approvable_id, action_at DESC") + Order("approvable_id, id DESC") var pfkID uint if err := r.db.WithContext(ctx). diff --git a/internal/modules/repports/controllers/repport.controller.go b/internal/modules/repports/controllers/repport.controller.go index e4b6088e..21d3c49a 100644 --- a/internal/modules/repports/controllers/repport.controller.go +++ b/internal/modules/repports/controllers/repport.controller.go @@ -2,7 +2,6 @@ package controller import ( "math" - "strconv" "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/dto" service "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/services" @@ -22,27 +21,35 @@ func NewRepportController(repportService service.RepportService) *RepportControl } } -func (c *RepportController) GetAll(ctx *fiber.Ctx) error { - query := &validation.Query{ - Page: ctx.QueryInt("page", 1), - Limit: ctx.QueryInt("limit", 10), - Search: ctx.Query("search", ""), +func (c *RepportController) GetExpense(ctx *fiber.Ctx) error { + query := &validation.ExpenseQuery{ + Page: ctx.QueryInt("page", 1), + Limit: ctx.QueryInt("limit", 10), + Search: ctx.Query("search", ""), + Category: ctx.Query("category", ""), + SupplierId: int64(ctx.QueryInt("supplier_id", 0)), + KandangId: int64(ctx.QueryInt("kandang_id", 0)), + ProjectFlockKandangId: int64(ctx.QueryInt("project_flock_kandang_id", 0)), + NonstockId: int64(ctx.QueryInt("nonstock_id", 0)), + AreaId: int64(ctx.QueryInt("area_id", 0)), + LocationId: int64(ctx.QueryInt("location_id", 0)), + RealizationDate: ctx.Query("realization_date", ""), } if query.Page < 1 || query.Limit < 1 { return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0") } - result, totalResults, err := c.RepportService.GetAll(ctx, query) + result, totalResults, err := c.RepportService.GetExpense(ctx, query) if err != nil { return err } return ctx.Status(fiber.StatusOK). - JSON(response.SuccessWithPaginate[dto.RepportListDTO]{ + JSON(response.SuccessWithPaginate[dto.RepportExpenseListDTO]{ Code: fiber.StatusOK, Status: "success", - Message: "Get all reports successfully", + Message: "Get expense report successfully", Meta: response.Meta{ Page: query.Page, Limit: query.Limit, @@ -53,46 +60,40 @@ func (c *RepportController) GetAll(ctx *fiber.Ctx) error { }) } -func (c *RepportController) GetOne(ctx *fiber.Ctx) error { - param := ctx.Params("id") - - id, err := strconv.Atoi(param) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, "Invalid Id") +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)), } - result, err := c.RepportService.GetOne(ctx, uint(id)) + if query.Page < 1 || query.Limit < 1 { + return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0") + } + + result, totalResults, err := c.RepportService.GetMarketing(ctx, query) if err != nil { return err } return ctx.Status(fiber.StatusOK). - JSON(response.Success{ + JSON(response.SuccessWithPaginate[dto.RepportMarketingListDTO]{ Code: fiber.StatusOK, Status: "success", - Message: "Get report successfully", - Data: result, - }) -} - -func (c *RepportController) GetExpense(ctx *fiber.Ctx) error { - param := ctx.Params("id") - - id, err := strconv.Atoi(param) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, "Invalid Id") - } - - result, err := c.RepportService.GetOne(ctx, uint(id)) - if err != nil { - return err - } - - return ctx.Status(fiber.StatusOK). - JSON(response.Success{ - Code: fiber.StatusOK, - Status: "success", - Message: "Get report successfully", - Data: result, + Message: "Get marketing report successfully", + Meta: response.Meta{ + Page: query.Page, + Limit: query.Limit, + TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))), + TotalResults: totalResults, + }, + Data: result, }) } diff --git a/internal/modules/repports/dto/repport.dto.go b/internal/modules/repports/dto/repport.dto.go deleted file mode 100644 index 154c6f47..00000000 --- a/internal/modules/repports/dto/repport.dto.go +++ /dev/null @@ -1,16 +0,0 @@ -package dto - -import "time" - -// === DTO Structs === - -type RepportListDTO struct { - Id uint `json:"id"` - Name string `json:"name"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` -} - -type RepportDetailDTO struct { - RepportListDTO -} diff --git a/internal/modules/repports/dto/repportExpense.dto.go b/internal/modules/repports/dto/repportExpense.dto.go new file mode 100644 index 00000000..3e71df2c --- /dev/null +++ b/internal/modules/repports/dto/repportExpense.dto.go @@ -0,0 +1,179 @@ +package dto + +import ( + "time" + + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto" + kandangDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto" + nonstockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/nonstocks/dto" + supplierDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/dto" +) + +// === DTO Structs === + +type RepportExpenseBaseDTO struct { + Id uint64 `json:"id"` + ReferenceNumber string `json:"reference_number"` + PoNumber string `json:"po_number"` + Category string `json:"category"` + Supplier *supplierDTO.SupplierRelationDTO `json:"supplier,omitempty"` + RealizationDate *time.Time `json:"realization_date,omitempty"` + TransactionDate time.Time `json:"transaction_date"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type RepportExpensePengajuanDTO struct { + Id uint64 `json:"id"` + ExpenseId *uint64 `json:"expense_id,omitempty"` + ProjectFlockKandangId *uint64 `json:"project_flock_kandang_id,omitempty"` + Qty float64 `json:"qty"` + Price float64 `json:"price"` + Notes string `json:"notes"` + Nonstock *nonstockDTO.NonstockRelationDTO `json:"nonstock,omitempty"` + CreatedAt time.Time `json:"created_at"` +} + +type RepportExpenseRealisasiDTO struct { + Id *uint64 `json:"id,omitempty"` + ExpenseNonstockId *uint64 `json:"expense_nonstock_id,omitempty"` + Qty float64 `json:"qty"` + Price float64 `json:"price"` + Notes string `json:"notes"` + Nonstock *nonstockDTO.NonstockRelationDTO `json:"nonstock,omitempty"` + CreatedAt time.Time `json:"created_at"` +} + +type RepportExpenseListDTO struct { + RepportExpenseBaseDTO + Kandang *kandangDTO.KandangRelationDTO `json:"kandang,omitempty"` + Pengajuan RepportExpensePengajuanDTO `json:"pengajuan"` + Realisasi RepportExpenseRealisasiDTO `json:"realisasi"` + TotalPengajuan float64 `json:"total_pengajuan"` + TotalRealisasi float64 `json:"total_realisasi"` + LatestApproval *approvalDTO.ApprovalRelationDTO `json:"latest_approval,omitempty"` +} + +// === MAPPERS === + +func ToRepportExpenseBaseDTO(e *entity.Expense) RepportExpenseBaseDTO { + var realizationDate *time.Time + if !e.RealizationDate.IsZero() { + realizationDate = &e.RealizationDate + } + + var supplier *supplierDTO.SupplierRelationDTO + if e.Supplier != nil && e.Supplier.Id != 0 { + mapped := supplierDTO.ToSupplierRelationDTO(*e.Supplier) + supplier = &mapped + } + + return RepportExpenseBaseDTO{ + Id: e.Id, + ReferenceNumber: e.ReferenceNumber, + PoNumber: e.PoNumber, + Category: e.Category, + Supplier: supplier, + RealizationDate: realizationDate, + TransactionDate: e.TransactionDate, + CreatedAt: e.CreatedAt, + UpdatedAt: e.UpdatedAt, + } +} + +func ToRepportExpensePengajuanDTO(ns *entity.ExpenseNonstock) RepportExpensePengajuanDTO { + var nonstock *nonstockDTO.NonstockRelationDTO + if ns.Nonstock != nil && ns.Nonstock.Id != 0 { + mapped := nonstockDTO.ToNonstockRelationDTO(*ns.Nonstock) + nonstock = &mapped + } + + return RepportExpensePengajuanDTO{ + Id: ns.Id, + ExpenseId: ns.ExpenseId, + ProjectFlockKandangId: ns.ProjectFlockKandangId, + Qty: ns.Qty, + Price: ns.Price, + Notes: ns.Notes, + Nonstock: nonstock, + CreatedAt: ns.CreatedAt, + } +} + +func ToRepportExpenseRealisasiDTO(r *entity.ExpenseRealization) RepportExpenseRealisasiDTO { + var nonstock *nonstockDTO.NonstockRelationDTO + if r.ExpenseNonstock != nil && r.ExpenseNonstock.Nonstock != nil && r.ExpenseNonstock.Nonstock.Id != 0 { + mapped := nonstockDTO.ToNonstockRelationDTO(*r.ExpenseNonstock.Nonstock) + nonstock = &mapped + } + + return RepportExpenseRealisasiDTO{ + Id: r.ExpenseNonstockId, + ExpenseNonstockId: r.ExpenseNonstockId, + Qty: r.Qty, + Price: r.Price, + Notes: r.Notes, + Nonstock: nonstock, + CreatedAt: r.CreatedAt, + } +} + +func ToRepportExpenseListDTO(baseDTO RepportExpenseBaseDTO, ns *entity.ExpenseNonstock, latestApproval *approvalDTO.ApprovalRelationDTO) RepportExpenseListDTO { + var realisasi RepportExpenseRealisasiDTO + if ns.Realization != nil { + realisasi = ToRepportExpenseRealisasiDTO(ns.Realization) + } + + totalPengajuan := ns.Qty * ns.Price + totalRealisasi := float64(0) + if ns.Realization != nil { + totalRealisasi = ns.Realization.Qty * ns.Realization.Price + } + + // Get kandang data at the main level + var kandang *kandangDTO.KandangRelationDTO + if ns.Kandang != nil && ns.Kandang.Id != 0 { + mapped := kandangDTO.ToKandangRelationDTO(*ns.Kandang) + kandang = &mapped + } + + return RepportExpenseListDTO{ + RepportExpenseBaseDTO: baseDTO, + Kandang: kandang, + Pengajuan: ToRepportExpensePengajuanDTO(ns), + Realisasi: realisasi, + TotalPengajuan: totalPengajuan, + TotalRealisasi: totalRealisasi, + LatestApproval: latestApproval, + } +} + +func ToRepportExpenseListDTOs(realizations []entity.ExpenseRealization) []RepportExpenseListDTO { + result := make([]RepportExpenseListDTO, 0, len(realizations)) + + for _, realization := range realizations { + if realization.ExpenseNonstock == nil || realization.ExpenseNonstock.Expense == nil { + continue + } + + expense := realization.ExpenseNonstock.Expense + baseDTO := ToRepportExpenseBaseDTO(expense) + + var latestApproval *approvalDTO.ApprovalRelationDTO + if expense.LatestApproval != nil { + mapped := approvalDTO.ToApprovalDTO(*expense.LatestApproval) + latestApproval = &mapped + } + + // Create a temporary realization with the current realization data + if realization.ExpenseNonstock.Realization == nil { + realization.ExpenseNonstock.Realization = &realization + } + + dto := ToRepportExpenseListDTO(baseDTO, realization.ExpenseNonstock, latestApproval) + result = append(result, dto) + } + + return result +} diff --git a/internal/modules/repports/dto/repportMarketing.dto.go b/internal/modules/repports/dto/repportMarketing.dto.go new file mode 100644 index 00000000..9cbd57ba --- /dev/null +++ b/internal/modules/repports/dto/repportMarketing.dto.go @@ -0,0 +1,219 @@ +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" + userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" +) + +// === DTO Structs === + +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 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"` +} + +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"` +} + +// === MAPPERS === + +func ToRepportMarketingBaseDTO(m *entity.Marketing) RepportMarketingBaseDTO { + if m == nil { + return RepportMarketingBaseDTO{} + } + + var customer *customerDTO.CustomerRelationDTO + if m.Customer.Id != 0 { + mapped := customerDTO.ToCustomerRelationDTO(m.Customer) + customer = &mapped + } + + var salesPerson *userDTO.UserRelationDTO + if m.SalesPerson.Id != 0 { + mapped := userDTO.ToUserRelationDTO(m.SalesPerson) + salesPerson = &mapped + } + + 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{} + } + + var product *productDTO.ProductRelationDTO + if mp.ProductWarehouse.Product.Id != 0 { + mapped := productDTO.ToProductRelationDTO(mp.ProductWarehouse.Product) + product = &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{} + } + + var product *productDTO.ProductRelationDTO + if mdp.MarketingProduct.ProductWarehouse.Product.Id != 0 { + mapped := productDTO.ToProductRelationDTO(mdp.MarketingProduct.ProductWarehouse.Product) + 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(), + } +} + +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, + } +} + +func ToRepportMarketingListDTOs(deliveryProducts []entity.MarketingDeliveryProduct) []RepportMarketingListDTO { + result := make([]RepportMarketingListDTO, 0, len(deliveryProducts)) + + 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 + } + } + + 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 +} diff --git a/internal/modules/repports/module.go b/internal/modules/repports/module.go index be0ba7a3..4479b733 100644 --- a/internal/modules/repports/module.go +++ b/internal/modules/repports/module.go @@ -5,19 +5,24 @@ import ( "github.com/gofiber/fiber/v2" "gorm.io/gorm" + commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" + approvalService "gitlab.com/mbugroup/lti-api.git/internal/common/service" sRepport "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/services" expenseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories" + marketingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories" ) type RepportModule struct{} func (RepportModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) { - // Initialize expense realization repository - expRealizationRepo := expenseRepo.NewExpenseRealizationRepository(db) - // Initialize report service with expense realization repo - repportService := sRepport.NewRepportService(validate, expRealizationRepo) + expenseRealizationRepository := expenseRepo.NewExpenseRealizationRepository(db) + marketingDeliveryProductRepository := marketingRepo.NewMarketingDeliveryProductRepository(db) + approvalRepository := commonRepo.NewApprovalRepository(db) + + approvalSvc := approvalService.NewApprovalService(approvalRepository) + repportService := sRepport.NewRepportService(validate, expenseRealizationRepository, marketingDeliveryProductRepository, approvalSvc) RepportRoutes(router, repportService) } diff --git a/internal/modules/repports/route.go b/internal/modules/repports/route.go index d01fd4b2..4aea831c 100644 --- a/internal/modules/repports/route.go +++ b/internal/modules/repports/route.go @@ -1,7 +1,6 @@ package repports import ( - controller "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/controllers" repport "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/services" @@ -11,10 +10,8 @@ import ( func RepportRoutes(v1 fiber.Router, s repport.RepportService) { ctrl := controller.NewRepportController(s) - route := v1.Group("/repports") + route := v1.Group("/reports") - route.Get("/", ctrl.GetAll) - route.Get("/:id", ctrl.GetOne) - - route.Get("expense", ctrl.GetExpense) + route.Get("/expense", ctrl.GetExpense) + route.Get("/marketing", ctrl.GetMarketing) } diff --git a/internal/modules/repports/services/repport.service.go b/internal/modules/repports/services/repport.service.go index 82fd5470..3adc5c0a 100644 --- a/internal/modules/repports/services/repport.service.go +++ b/internal/modules/repports/services/repport.service.go @@ -1,106 +1,115 @@ package service import ( - "strings" - "time" - "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" + approvalService "gitlab.com/mbugroup/lti-api.git/internal/common/service" + approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto" expenseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories" + marketingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories" "github.com/go-playground/validator/v10" "github.com/gofiber/fiber/v2" "github.com/sirupsen/logrus" + "gorm.io/gorm" ) type RepportService interface { - GetAll(ctx *fiber.Ctx, params *validation.Query) ([]dto.RepportListDTO, int64, error) - GetOne(ctx *fiber.Ctx, id uint) (*dto.RepportListDTO, error) - GetExpense(ctx *fiber.Ctx, id uint) (*dto.RepportListDTO, error) + GetExpense(ctx *fiber.Ctx, params *validation.ExpenseQuery) ([]dto.RepportExpenseListDTO, int64, error) + GetMarketing(ctx *fiber.Ctx, params *validation.MarketingQuery) ([]dto.RepportMarketingListDTO, int64, error) } type repportService struct { Log *logrus.Logger Validate *validator.Validate - dummyData map[uint]dto.RepportListDTO ExpenseRealizationRepo expenseRepo.ExpenseRealizationRepository + MarketingDeliveryRepo marketingRepo.MarketingDeliveryProductRepository + ApprovalSvc approvalService.ApprovalService } -func NewRepportService(validate *validator.Validate, expenseRealizationRepo expenseRepo.ExpenseRealizationRepository) RepportService { - // Initialize with dummy data - now := time.Now() - dummyData := map[uint]dto.RepportListDTO{ - 1: { - Id: 1, - Name: "Sales Report", - CreatedAt: now, - UpdatedAt: now, - }, - 2: { - Id: 2, - Name: "Inventory Report", - CreatedAt: now, - UpdatedAt: now, - }, - 3: { - Id: 3, - Name: "Production Report", - CreatedAt: now, - UpdatedAt: now, - }, - } - +func NewRepportService(validate *validator.Validate, expenseRealizationRepo expenseRepo.ExpenseRealizationRepository, marketingDeliveryRepo marketingRepo.MarketingDeliveryProductRepository, approvalSvc approvalService.ApprovalService) RepportService { return &repportService{ Log: utils.Log, Validate: validate, - dummyData: dummyData, ExpenseRealizationRepo: expenseRealizationRepo, + MarketingDeliveryRepo: marketingDeliveryRepo, + ApprovalSvc: approvalSvc, } } -func (s *repportService) GetAll(c *fiber.Ctx, params *validation.Query) ([]dto.RepportListDTO, int64, error) { +func (s *repportService) GetExpense(c *fiber.Ctx, params *validation.ExpenseQuery) ([]dto.RepportExpenseListDTO, int64, error) { if err := s.Validate.Struct(params); err != nil { return nil, 0, err } - // Convert map to slice - var results []dto.RepportListDTO - for _, v := range s.dummyData { - // Apply search filter if provided - if params.Search != "" && !strings.Contains(strings.ToLower(v.Name), strings.ToLower(params.Search)) { - continue - } - results = append(results, v) - } - - // Apply pagination - total := int64(len(results)) offset := (params.Page - 1) * params.Limit - if offset >= int(total) { - return []dto.RepportListDTO{}, total, nil + realizations, total, err := s.ExpenseRealizationRepo.GetAllWithFilters(c.Context(), offset, params.Limit, params) + if err != nil { + s.Log.Errorf("GetAllWithFilters error: %v", err) + return nil, 0, err } - end := offset + params.Limit - if end > int(total) { - end = int(total) + result := dto.ToRepportExpenseListDTOs(realizations) + + expenseIDs := make([]uint, 0, len(result)) + for i := range result { + expenseIDs = append(expenseIDs, uint(result[i].Id)) } - return results[offset:end], total, nil + approvals, err := s.ApprovalSvc.LatestByTargets(c.Context(), utils.ApprovalWorkflowExpense, expenseIDs, func(db *gorm.DB) *gorm.DB { + return db.Preload("ActionUser") + }) + if err != nil { + s.Log.Warnf("LatestByTargets error: %v", err) + } + + for i := range result { + expenseIDAsUint := uint(result[i].Id) + if approval, exists := approvals[expenseIDAsUint]; exists && approval != nil { + mapped := approvalDTO.ToApprovalDTO(*approval) + result[i].LatestApproval = &mapped + } + } + + return result, total, nil } -func (s *repportService) GetOne(c *fiber.Ctx, id uint) (*dto.RepportListDTO, error) { - if data, ok := s.dummyData[id]; ok { - return &data, nil +func (s *repportService) GetMarketing(c *fiber.Ctx, params *validation.MarketingQuery) ([]dto.RepportMarketingListDTO, int64, error) { + if err := s.Validate.Struct(params); err != nil { + return nil, 0, err } - return nil, fiber.NewError(fiber.StatusNotFound, "Report not found") -} -func (s *repportService) GetExpense(c *fiber.Ctx, id uint) (*dto.RepportListDTO, error) { - if data, ok := s.dummyData[id]; ok { - return &data, nil + offset := (params.Page - 1) * params.Limit + + deliveryProducts, total, err := s.MarketingDeliveryRepo.GetAllWithFilters(c.Context(), offset, params.Limit, params) + if err != nil { + return nil, 0, err } - return nil, fiber.NewError(fiber.StatusNotFound, "Report not found") + + 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 + } + } + + 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 } diff --git a/internal/modules/repports/validations/repport.validation.go b/internal/modules/repports/validations/repport.validation.go index a7ec4a6d..7efc51f9 100644 --- a/internal/modules/repports/validations/repport.validation.go +++ b/internal/modules/repports/validations/repport.validation.go @@ -1,7 +1,29 @@ package validation -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"` +type ExpenseQuery 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"` + Category string `query:"category" validate:"omitempty,oneof=BOP NON-BOP"` + SupplierId int64 `query:"supplier_id" validate:"omitempty"` + KandangId int64 `query:"kandang_id" validate:"omitempty"` + ProjectFlockKandangId int64 `query:"project_flock_kandang_id" validate:"omitempty"` + ProjectFlockId int64 `query:"project_flock_id" validate:"omitempty"` + NonstockId int64 `query:"nonstock_id" validate:"omitempty"` + AreaId int64 `query:"area_id" validate:"omitempty"` + LocationId int64 `query:"location_id" validate:"omitempty"` + RealizationDate string `query:"realization_date" validate:"omitempty"` +} + +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"` }