From 126294d288a84b691217eef2085e82d55e0fb12a Mon Sep 17 00:00:00 2001 From: giovanni Date: Thu, 7 May 2026 22:39:36 +0700 Subject: [PATCH] add api get list stock log by product warehouse id --- internal/middleware/permissions.go | 1 + internal/modules/inventory/route.go | 2 + .../controllers/stock-log.controller.go | 66 +++++++++ .../controllers/stock-log.export.go | 118 +++++++++++++++++ .../inventory/stock-logs/dto/stock-log.dto.go | 61 +++++++++ .../modules/inventory/stock-logs/module.go | 24 ++++ .../modules/inventory/stock-logs/route.go | 19 +++ .../stock-logs/services/stock-log.service.go | 125 ++++++++++++++++++ .../validations/stock-log.validation.go | 7 + 9 files changed, 423 insertions(+) create mode 100644 internal/modules/inventory/stock-logs/controllers/stock-log.controller.go create mode 100644 internal/modules/inventory/stock-logs/controllers/stock-log.export.go create mode 100644 internal/modules/inventory/stock-logs/dto/stock-log.dto.go create mode 100644 internal/modules/inventory/stock-logs/module.go create mode 100644 internal/modules/inventory/stock-logs/route.go create mode 100644 internal/modules/inventory/stock-logs/services/stock-log.service.go create mode 100644 internal/modules/inventory/stock-logs/validations/stock-log.validation.go diff --git a/internal/middleware/permissions.go b/internal/middleware/permissions.go index a29ccd91..1e6165c8 100644 --- a/internal/middleware/permissions.go +++ b/internal/middleware/permissions.go @@ -66,6 +66,7 @@ const ( P_ProductStockGetOne = "lti.inventory.product_stock.detail" P_ProductWarehousekGetAll = "lti.inventory.product_warehouses.list" P_ProductWarehouseGetOne = "lti.inventory.product_warehouses.detail" + P_StockLogGetAll = "lti.inventory.stock_log.list" ) const ( P_ClosingGetAll = "lti.closing.list" diff --git a/internal/modules/inventory/route.go b/internal/modules/inventory/route.go index 0d4d2f4b..bdff88f0 100644 --- a/internal/modules/inventory/route.go +++ b/internal/modules/inventory/route.go @@ -10,6 +10,7 @@ import ( adjustments "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments" productStocks "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-stocks" productWarehouses "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses" + stockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/stock-logs" transfers "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers" // MODULE IMPORTS ) @@ -23,6 +24,7 @@ func RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Valida adjustments.AdjustmentModule{}, transfers.TransferModule{}, productStocks.ProductStockModule{}, + stockLogs.StockLogModule{}, // MODULE REGISTRY } diff --git a/internal/modules/inventory/stock-logs/controllers/stock-log.controller.go b/internal/modules/inventory/stock-logs/controllers/stock-log.controller.go new file mode 100644 index 00000000..762a98fd --- /dev/null +++ b/internal/modules/inventory/stock-logs/controllers/stock-log.controller.go @@ -0,0 +1,66 @@ +package controller + +import ( + "math" + "strings" + + validation "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/stock-logs/validations" + stockLogDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/stock-logs/dto" + stockLogService "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/stock-logs/services" + "gitlab.com/mbugroup/lti-api.git/internal/response" + + "github.com/gofiber/fiber/v2" +) + +type StockLogController struct { + StockLogService stockLogService.StockLogService +} + +func NewStockLogController(s stockLogService.StockLogService) *StockLogController { + return &StockLogController{ + StockLogService: s, + } +} + +func (u *StockLogController) GetAll(c *fiber.Ctx) error { + query := &validation.Query{ + Page: c.QueryInt("page", 1), + Limit: c.QueryInt("limit", 10), + ProductWarehouseID: uint(c.QueryInt("product_warehouse_id", 0)), + } + + if query.Page < 1 || query.Limit < 1 { + return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0") + } + + // Export to Excel + if strings.EqualFold(strings.TrimSpace(c.Query("export")), "excel") { + if query.ProductWarehouseID == 0 { + return fiber.NewError(fiber.StatusBadRequest, "product_warehouse_id is required for export") + } + results, err := u.StockLogService.GetAllForExport(c, query.ProductWarehouseID) + if err != nil { + return err + } + return exportStockLogListExcel(c, results) + } + + result, totalResults, err := u.StockLogService.GetAll(c, query) + if err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.SuccessWithPaginate[stockLogDTO.StockLogListDTO]{ + Code: fiber.StatusOK, + Status: "success", + Message: "Get all stock logs successfully", + Meta: response.Meta{ + Page: query.Page, + Limit: query.Limit, + TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))), + TotalResults: totalResults, + }, + Data: stockLogDTO.ToStockLogListDTOs(result), + }) +} diff --git a/internal/modules/inventory/stock-logs/controllers/stock-log.export.go b/internal/modules/inventory/stock-logs/controllers/stock-log.export.go new file mode 100644 index 00000000..652ef477 --- /dev/null +++ b/internal/modules/inventory/stock-logs/controllers/stock-log.export.go @@ -0,0 +1,118 @@ +package controller + +import ( + "fmt" + "time" + + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + + "github.com/gofiber/fiber/v2" + "github.com/xuri/excelize/v2" +) + +func exportStockLogListExcel(c *fiber.Ctx, stockLogs []entity.StockLog) error { + file := excelize.NewFile() + defer file.Close() + + sheet := "Stock Logs" + file.SetSheetName("Sheet1", sheet) + + headers := []string{ + "ID", + "Tanggal", + "Gudang", + "Stok Akhir", + "Peningkatan", + "Penurunan", + "Jenis Transaksi", + "Catatan", + "Oleh", + } + + // Column widths + colWidths := map[string]float64{ + "A": 8, + "B": 20, + "C": 25, + "D": 14, + "E": 14, + "F": 14, + "G": 20, + "H": 30, + "I": 20, + } + for col, width := range colWidths { + file.SetColWidth(sheet, col, col, width) + } + + // Header style + headerStyle, _ := file.NewStyle(&excelize.Style{ + Font: &excelize.Font{ + Bold: true, + Size: 11, + }, + Fill: excelize.Fill{ + Type: "pattern", + Pattern: 1, + Color: []string{"D9E1F2"}, + }, + Border: []excelize.Border{ + {Type: "bottom", Style: 1, Color: "000000"}, + }, + }) + + // Write header row + for i, h := range headers { + cell, _ := excelize.CoordinatesToCellName(i+1, 1) + file.SetCellValue(sheet, cell, h) + file.SetCellStyle(sheet, cell, cell, headerStyle) + } + + // Freeze header row + file.SetPanes(sheet, &excelize.Panes{ + Freeze: true, + YSplit: 1, + TopLeftCell: "A2", + ActivePane: "bottomLeft", + }) + + // Write data rows + for i, log := range stockLogs { + row := i + 2 + + warehouseName := "" + if log.ProductWarehouse != nil { + warehouseName = log.ProductWarehouse.Warehouse.Name + } + + userName := "" + if log.CreatedUser != nil { + userName = log.CreatedUser.Name + } + + notes := "" + if log.Notes != "" { + notes = log.Notes + } + + file.SetCellInt(sheet, fmt.Sprintf("A%d", row), int(log.Id)) + file.SetCellValue(sheet, fmt.Sprintf("B%d", row), log.CreatedAt.Format("2006-01-02 15:04:05")) + file.SetCellValue(sheet, fmt.Sprintf("C%d", row), warehouseName) + file.SetCellFloat(sheet, fmt.Sprintf("D%d", row), log.Stock, 3, 64) + file.SetCellFloat(sheet, fmt.Sprintf("E%d", row), log.Increase, 3, 64) + file.SetCellFloat(sheet, fmt.Sprintf("F%d", row), log.Decrease, 3, 64) + file.SetCellValue(sheet, fmt.Sprintf("G%d", row), log.LoggableType) + file.SetCellValue(sheet, fmt.Sprintf("H%d", row), notes) + file.SetCellValue(sheet, fmt.Sprintf("I%d", row), userName) + } + + buffer, err := file.WriteToBuffer() + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "failed to generate excel file") + } + + filename := fmt.Sprintf("stock_logs_%s.xlsx", time.Now().Format("20060102_150405")) + c.Set("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") + c.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, filename)) + return c.Status(fiber.StatusOK).Send(buffer.Bytes()) +} diff --git a/internal/modules/inventory/stock-logs/dto/stock-log.dto.go b/internal/modules/inventory/stock-logs/dto/stock-log.dto.go new file mode 100644 index 00000000..732df6fe --- /dev/null +++ b/internal/modules/inventory/stock-logs/dto/stock-log.dto.go @@ -0,0 +1,61 @@ +package dto + +import ( + "time" + + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" +) + +type StockLogListDTO struct { + Id uint `json:"id"` + ProductWarehouseId uint `json:"product_warehouse_id"` + Increase float64 `json:"increase"` + Decrease float64 `json:"decrease"` + Stock float64 `json:"stock"` + LoggableType string `json:"loggable_type"` + LoggableId uint `json:"loggable_id"` + Notes *string `json:"notes"` + CreatedBy uint `json:"created_by"` + CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"` + CreatedAt time.Time `json:"created_at"` +} + +func ToStockLogListDTO(e entity.StockLog) StockLogListDTO { + var notes *string + if e.Notes != "" { + n := e.Notes + notes = &n + } + + var createdUser *userDTO.UserRelationDTO + if e.CreatedUser != nil && e.CreatedUser.Id != 0 { + mapped := userDTO.ToUserRelationDTO(*e.CreatedUser) + createdUser = &mapped + } + + return StockLogListDTO{ + Id: e.Id, + ProductWarehouseId: e.ProductWarehouseId, + Increase: e.Increase, + Decrease: e.Decrease, + Stock: e.Stock, + LoggableType: e.LoggableType, + LoggableId: e.LoggableId, + Notes: notes, + CreatedBy: e.CreatedBy, + CreatedUser: createdUser, + CreatedAt: e.CreatedAt, + } +} + +func ToStockLogListDTOs(e []entity.StockLog) []StockLogListDTO { + if len(e) == 0 { + return []StockLogListDTO{} + } + result := make([]StockLogListDTO, len(e)) + for i, log := range e { + result[i] = ToStockLogListDTO(log) + } + return result +} diff --git a/internal/modules/inventory/stock-logs/module.go b/internal/modules/inventory/stock-logs/module.go new file mode 100644 index 00000000..2fc6815a --- /dev/null +++ b/internal/modules/inventory/stock-logs/module.go @@ -0,0 +1,24 @@ +package stockLogs + +import ( + "github.com/go-playground/validator/v10" + "github.com/gofiber/fiber/v2" + "gorm.io/gorm" + + stockLogService "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/stock-logs/services" + stockLogRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories" + rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" + sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" +) + +type StockLogModule struct{} + +func (StockLogModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) { + userRepo := rUser.NewUserRepository(db) + userService := sUser.NewUserService(userRepo, validate) + + stockLogRepo := stockLogRepo.NewStockLogRepository(db) + service := stockLogService.NewStockLogService(stockLogRepo, validate) + + StockLogRoutes(router, userService, service) +} diff --git a/internal/modules/inventory/stock-logs/route.go b/internal/modules/inventory/stock-logs/route.go new file mode 100644 index 00000000..a7387013 --- /dev/null +++ b/internal/modules/inventory/stock-logs/route.go @@ -0,0 +1,19 @@ +package stockLogs + +import ( + m "gitlab.com/mbugroup/lti-api.git/internal/middleware" + controller "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/stock-logs/controllers" + stockLog "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/stock-logs/services" + user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" + + "github.com/gofiber/fiber/v2" +) + +func StockLogRoutes(v1 fiber.Router, u user.UserService, s stockLog.StockLogService) { + ctrl := controller.NewStockLogController(s) + + route := v1.Group("/stock-logs") + route.Use(m.Auth(u)) + + route.Get("/", m.RequirePermissions(m.P_StockLogGetAll), ctrl.GetAll) +} diff --git a/internal/modules/inventory/stock-logs/services/stock-log.service.go b/internal/modules/inventory/stock-logs/services/stock-log.service.go new file mode 100644 index 00000000..77639fbd --- /dev/null +++ b/internal/modules/inventory/stock-logs/services/stock-log.service.go @@ -0,0 +1,125 @@ +package service + +import ( + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + m "gitlab.com/mbugroup/lti-api.git/internal/middleware" + validation "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/stock-logs/validations" + stockLogRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories" + "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 StockLogService interface { + GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.StockLog, int64, error) + GetAllForExport(ctx *fiber.Ctx, productWarehouseID uint) ([]entity.StockLog, error) +} + +type stockLogService struct { + Log *logrus.Logger + Validate *validator.Validate + StockLogRepo stockLogRepo.StockLogRepository +} + +func NewStockLogService( + stockLogRepo stockLogRepo.StockLogRepository, + validate *validator.Validate, +) StockLogService { + return &stockLogService{ + Log: utils.Log, + Validate: validate, + StockLogRepo: stockLogRepo, + } +} + +func (s *stockLogService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.StockLog, int64, error) { + if err := s.Validate.Struct(params); err != nil { + return nil, 0, err + } + + locationScope, areaScope, err := m.ResolveLocationAreaScopes(c, s.StockLogRepo.DB()) + if err != nil { + return nil, 0, err + } + + offset := (params.Page - 1) * params.Limit + + stockLogs, total, err := s.StockLogRepo.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { + db = db.Where("product_warehouse_id = ?", params.ProductWarehouseID) + + if locationScope.Restrict || areaScope.Restrict { + if (locationScope.Restrict && len(locationScope.IDs) == 0) || (areaScope.Restrict && len(areaScope.IDs) == 0) { + return db.Where("1 = 0") + } + db = db. + Joins("JOIN product_warehouses pw ON pw.id = stock_logs.product_warehouse_id"). + Joins("JOIN warehouses w ON w.id = pw.warehouse_id") + if locationScope.Restrict { + db = db.Where("w.location_id IN ?", locationScope.IDs) + } + if areaScope.Restrict { + db = db.Where("w.area_id IN ?", areaScope.IDs) + } + } + + db = db. + Preload("CreatedUser"). + Order("stock_logs.created_at DESC") + + return db + }) + if err != nil { + s.Log.Errorf("Failed to get stock logs: %+v", err) + return nil, 0, err + } + + if total == 0 { + return []entity.StockLog{}, 0, nil + } + + return stockLogs, total, nil +} + +func (s *stockLogService) GetAllForExport(c *fiber.Ctx, productWarehouseID uint) ([]entity.StockLog, error) { + locationScope, areaScope, err := m.ResolveLocationAreaScopes(c, s.StockLogRepo.DB()) + if err != nil { + return nil, err + } + + stockLogs, _, err := s.StockLogRepo.GetAll(c.Context(), 0, -1, func(db *gorm.DB) *gorm.DB { + db = db.Where("product_warehouse_id = ?", productWarehouseID) + + if locationScope.Restrict || areaScope.Restrict { + if (locationScope.Restrict && len(locationScope.IDs) == 0) || (areaScope.Restrict && len(areaScope.IDs) == 0) { + return db.Where("1 = 0") + } + db = db. + Joins("JOIN product_warehouses pw ON pw.id = stock_logs.product_warehouse_id"). + Joins("JOIN warehouses w ON w.id = pw.warehouse_id") + if locationScope.Restrict { + db = db.Where("w.location_id IN ?", locationScope.IDs) + } + if areaScope.Restrict { + db = db.Where("w.area_id IN ?", areaScope.IDs) + } + } + + db = db. + Preload("CreatedUser"). + Preload("ProductWarehouse"). + Preload("ProductWarehouse.Warehouse"). + Order("stock_logs.created_at ASC") + + return db + }) + if err != nil { + s.Log.Errorf("Failed to get stock logs for export: %+v", err) + return nil, err + } + + return stockLogs, nil +} + diff --git a/internal/modules/inventory/stock-logs/validations/stock-log.validation.go b/internal/modules/inventory/stock-logs/validations/stock-log.validation.go new file mode 100644 index 00000000..a8af6d25 --- /dev/null +++ b/internal/modules/inventory/stock-logs/validations/stock-log.validation.go @@ -0,0 +1,7 @@ +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,gt=0"` + ProductWarehouseID uint `query:"product_warehouse_id" validate:"required,gt=0"` +}