diff --git a/internal/middleware/auth.go b/internal/middleware/auth.go index f243fe9a..03f9cb7d 100644 --- a/internal/middleware/auth.go +++ b/internal/middleware/auth.go @@ -3,13 +3,13 @@ package middleware import ( "strings" - "gitlab.com/mbugroup/lti-api.git/internal/config" + "github.com/gofiber/fiber/v2" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" "gitlab.com/mbugroup/lti-api.git/internal/modules/sso/session" service "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" "gitlab.com/mbugroup/lti-api.git/internal/sso" "gitlab.com/mbugroup/lti-api.git/internal/utils" - "github.com/gofiber/fiber/v2" + "gitlab.com/mbugroup/lti-api.git/internal/config" ) const ( @@ -199,7 +199,6 @@ func hasAllScopes(have, required []string) bool { return true } - // RequirePermissions ensures the authenticated user possesses all specified permissions. func RequirePermissions(perms ...string) fiber.Handler { required := canonicalPermissions(perms) diff --git a/internal/middleware/permissions.go b/internal/middleware/permissions.go index 30f1b35a..928242a0 100644 --- a/internal/middleware/permissions.go +++ b/internal/middleware/permissions.go @@ -7,8 +7,8 @@ const ( //recording const ( - PermissionRecordingRead = "recording.read" - PermissionRecordingCreate = "recording.write" + PermissionRecordingRead = "recording.index" + PermissionRecordingCreate = "recording.create" PermissionRecordingUpdate = "recording.update" PermissionRecordingDelete = "recording.delete" ) \ No newline at end of file diff --git a/internal/modules/expenses/repositories/expense.repository.go b/internal/modules/expenses/repositories/expense.repository.go index 3e2a84b1..f3a50d33 100644 --- a/internal/modules/expenses/repositories/expense.repository.go +++ b/internal/modules/expenses/repositories/expense.repository.go @@ -15,8 +15,8 @@ type ExpenseRepository interface { IdExists(ctx context.Context, id uint) (bool, error) GetNextSequence(ctx context.Context) (int, error) GetWithSupplier(ctx context.Context, id uint64) (*entity.Expense, error) - WithProjectFlockKandangFilter(pfkID uint) func(*gorm.DB) *gorm.DB - CountUnfinishedByProjectFlockKandang(ctx context.Context, pfkID uint, isFinished func(*entity.Approval) bool) (int64, error) + WithProjectFlockKandangFilter(pfkID, kandangID uint) func(*gorm.DB) *gorm.DB + CountUnfinishedByProjectFlockKandang(ctx context.Context, pfkID, kandangID uint, isFinished func(*entity.Approval) bool) (int64, error) } type ExpenseRepositoryImpl struct { @@ -54,25 +54,31 @@ func (r *ExpenseRepositoryImpl) GetWithSupplier(ctx context.Context, id uint64) return &expense, nil } -func (r *ExpenseRepositoryImpl) WithProjectFlockKandangFilter(pfkID uint) func(*gorm.DB) *gorm.DB { +func (r *ExpenseRepositoryImpl) WithProjectFlockKandangFilter(pfkID, kandangID uint) func(*gorm.DB) *gorm.DB { return func(db *gorm.DB) *gorm.DB { - if pfkID == 0 { + if pfkID == 0 && kandangID == 0 { return db } - return db.Joins("JOIN expense_nonstocks ON expense_nonstocks.expense_id = expenses.id"). - Where("expense_nonstocks.project_flock_kandang_id = ?", pfkID) + q := db.Joins("JOIN expense_nonstocks ON expense_nonstocks.expense_id = expenses.id") + if pfkID > 0 && kandangID > 0 { + return q.Where("expense_nonstocks.project_flock_kandang_id = ? OR expense_nonstocks.kandang_id = ?", pfkID, kandangID) + } + if pfkID > 0 { + return q.Where("expense_nonstocks.project_flock_kandang_id = ?", pfkID) + } + return q.Where("expense_nonstocks.kandang_id = ?", kandangID) } } -func (r *ExpenseRepositoryImpl) CountUnfinishedByProjectFlockKandang(ctx context.Context, pfkID uint, isFinished func(*entity.Approval) bool) (int64, error) { - if pfkID == 0 { +func (r *ExpenseRepositoryImpl) CountUnfinishedByProjectFlockKandang(ctx context.Context, pfkID, kandangID uint, isFinished func(*entity.Approval) bool) (int64, error) { + if pfkID == 0 && kandangID == 0 { return 0, nil } var ids []uint64 if err := r.DB().WithContext(ctx). Table("expenses"). - Scopes(r.WithProjectFlockKandangFilter(pfkID)). + Scopes(r.WithProjectFlockKandangFilter(pfkID, kandangID)). Group("expenses.id"). Pluck("expenses.id", &ids).Error; err != nil { return 0, err 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 641ce531..8b33a852 100644 --- a/internal/modules/inventory/product-warehouses/repositories/product_warehouse.repository.go +++ b/internal/modules/inventory/product-warehouses/repositories/product_warehouse.repository.go @@ -258,7 +258,10 @@ func (r *ProductWarehouseRepositoryImpl) GetByFlagAndWarehouseID(ctx context.Con Joins("JOIN flags ON flags.flagable_id = products.id AND flags.flagable_type = 'products'"). Where("flags.name = ? AND product_warehouses.warehouse_id = ?", flagName, warehouseId). Order("product_warehouses.id DESC"). - Preload("Product").Preload("Warehouse"). + Preload("Product"). + Preload("Product.ProductCategory"). + Preload("Product.Uom"). + Preload("Warehouse"). Find(&productWarehouses).Error if err != nil { return nil, err diff --git a/internal/modules/production/project-flock-kandangs/controllers/project_flock_kandang.controller.go b/internal/modules/production/project-flock-kandangs/controllers/project_flock_kandang.controller.go index dcdb9c82..dce7b02b 100644 --- a/internal/modules/production/project-flock-kandangs/controllers/project_flock_kandang.controller.go +++ b/internal/modules/production/project-flock-kandangs/controllers/project_flock_kandang.controller.go @@ -110,3 +110,22 @@ func (u *ProjectFlockKandangController) Closing(c *fiber.Ctx) error { Data: result, }) } + +func (u *ProjectFlockKandangController) CheckClosing(c *fiber.Ctx) error { + id, err := strconv.Atoi(c.Params("id")) + if err != nil || id <= 0 { + return fiber.NewError(fiber.StatusBadRequest, "Invalid Id") + } + + result, err := u.ProjectFlockKandangService.CheckClosing(c, uint(id)) + if err != nil { + return err + } + + return c.Status(fiber.StatusOK).JSON(response.Success{ + Code: fiber.StatusOK, + Status: "success", + Message: "Cek persyaratan closing kandang", + Data: result, + }) +} diff --git a/internal/modules/production/project-flock-kandangs/route.go b/internal/modules/production/project-flock-kandangs/route.go index 1105fad3..3998a324 100644 --- a/internal/modules/production/project-flock-kandangs/route.go +++ b/internal/modules/production/project-flock-kandangs/route.go @@ -22,5 +22,8 @@ func ProjectFlockKandangRoutes(v1 fiber.Router, u user.UserService, s projectFlo route.Get("/", ctrl.GetAll) route.Get("/:id", ctrl.GetOne) + // route.Post("/:id/closing", m.RequirePermissions(m.PermissionProjectFlockClosing), ctrl.Closing) + // route.Get("/:id/closing/check", m.RequirePermissions(m.PermissionProjectFlockClosing), ctrl.CheckClosing) route.Post("/:id/closing", ctrl.Closing) + route.Get("/:id/closing/check", ctrl.CheckClosing) } diff --git a/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go b/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go index 021f002e..bb0a9186 100644 --- a/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go +++ b/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go @@ -24,10 +24,10 @@ import ( type ProjectFlockKandangService interface { GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlockKandang, int64, error) GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectFlockKandang, map[uint]float64, []entity.ProductWarehouse, error) + CheckClosing(ctx *fiber.Ctx, id uint) (*ClosingCheckResult, error) Closing(ctx *fiber.Ctx, id uint, req *validation.Closing) (*entity.ProjectFlockKandang, error) } -// Note: map[uint]float64 adalah mapping dari ProductWarehouse ID ke calculated available quantity type projectFlockKandangService struct { Log *logrus.Logger @@ -40,6 +40,33 @@ type projectFlockKandangService struct { PopulationRepo repository.ProjectFlockPopulationRepository } +type ClosingCheckResult struct { + UnfinishedExpenses int64 `json:"unfinished_expenses"` + StockRemaining []StockRemainingDetail `json:"stock_remaining"` + Expenses []ExpenseSummary `json:"expenses"` +} + +type StockRemainingDetail struct { + FlagName string `json:"flag_name"` + ProductWarehouseId uint `json:"product_warehouse_id"` + ProductId uint `json:"product_id"` + ProductName string `json:"product_name"` + ProductCategory string `json:"product_category"` + Uom string `json:"uom"` + Quantity float64 `json:"quantity"` +} + +type ExpenseSummary struct { + Id uint64 `json:"id"` + PoNumber string `json:"po_number"` + Category string `json:"category"` + Total float64 `json:"total"` + Status string `json:"status"` + StepName string `json:"step_name"` + Step uint16 `json:"step"` + Reference string `json:"reference_number"` +} + func NewProjectFlockKandangService(repo repository.ProjectFlockKandangRepository, approvalSvc commonSvc.ApprovalService, expenseRepo expenseRepo.ExpenseRepository, warehouseRepo rWarehouse.WarehouseRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, populationRepo repository.ProjectFlockPopulationRepository, validate *validator.Validate) ProjectFlockKandangService { return &projectFlockKandangService{ Log: utils.Log, @@ -173,6 +200,127 @@ func (s projectFlockKandangService) getAvailableQuantities(c *fiber.Ctx, project return result, nil } +func (s projectFlockKandangService) CheckClosing(c *fiber.Ctx, id uint) (*ClosingCheckResult, error) { + pfk, err := s.Repository.GetByID(c.Context(), id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusNotFound, "ProjectFlockKandang not found") + } + return nil, err + } + + var unfinished int64 + if s.ExpenseRepo != nil && s.ApprovalSvc != nil { + count, err := s.ExpenseRepo.CountUnfinishedByProjectFlockKandang(c.Context(), pfk.Id, pfk.KandangId, func(appr *entity.Approval) bool { + return appr != nil && appr.StepNumber == uint16(utils.ExpenseStepSelesai) && appr.Action != nil && *appr.Action == entity.ApprovalActionApproved + }) + if err != nil { + return nil, err + } + unfinished = count + } + + stockRemain := make([]StockRemainingDetail, 0) + if s.WarehouseRepo != nil && s.ProductWarehouseRepo != nil { + warehouse, werr := s.WarehouseRepo.GetByKandangID(c.Context(), pfk.KandangId) + if werr != nil { + return nil, werr + } + + for _, flagName := range []utils.FlagType{utils.FlagPakan, utils.FlagOVK} { + productWarehouses, pwErr := s.ProductWarehouseRepo.GetByFlagAndWarehouseID(c.Context(), string(flagName), warehouse.Id) + if pwErr != nil { + return nil, pwErr + } + + for _, pw := range productWarehouses { + if pw.Quantity > 0 { + category := "" + if pw.Product.ProductCategory.Id != 0 { + category = pw.Product.ProductCategory.Name + } + uomName := "" + if pw.Product.Uom.Id != 0 { + uomName = pw.Product.Uom.Name + } + stockRemain = append(stockRemain, StockRemainingDetail{ + FlagName: string(flagName), + ProductWarehouseId: pw.Id, + ProductId: pw.ProductId, + ProductName: pw.Product.Name, + ProductCategory: category, + Uom: uomName, + Quantity: pw.Quantity, + }) + } + } + } + } + + expenseSummaries := make([]ExpenseSummary, 0) + if s.ExpenseRepo != nil { + var expenses []entity.Expense + if err := s.ExpenseRepo.DB().WithContext(c.Context()). + Scopes(s.ExpenseRepo.WithProjectFlockKandangFilter(pfk.Id, pfk.KandangId)). + Preload("Nonstocks"). + Find(&expenses).Error; err != nil { + return nil, err + } + + if len(expenses) > 0 && s.ApprovalSvc != nil { + ids := make([]uint, 0, len(expenses)) + for _, e := range expenses { + ids = append(ids, uint(e.Id)) + } + latestMap, err := s.ApprovalSvc.LatestByTargets(c.Context(), utils.ApprovalWorkflowExpense, ids, nil) + if err == nil { + for i := range expenses { + if latest, ok := latestMap[uint(expenses[i].Id)]; ok { + expenses[i].LatestApproval = latest + } + } + } + } + + for _, exp := range expenses { + total := 0.0 + for _, ns := range exp.Nonstocks { + total += ns.Qty * ns.Price + } + + status := "Pending" + stepName := "" + stepNum := uint16(0) + if exp.LatestApproval != nil { + stepName = exp.LatestApproval.StepName + stepNum = exp.LatestApproval.StepNumber + if exp.LatestApproval.Action != nil { + status = string(*exp.LatestApproval.Action) + } else if stepName != "" { + status = stepName + } + } + + expenseSummaries = append(expenseSummaries, ExpenseSummary{ + Id: exp.Id, + PoNumber: exp.PoNumber, + Category: exp.Category, + Total: total, + Status: status, + StepName: stepName, + Step: stepNum, + Reference: exp.ReferenceNumber, + }) + } + } + + return &ClosingCheckResult{ + UnfinishedExpenses: unfinished, + StockRemaining: stockRemain, + Expenses: expenseSummaries, + }, nil +} + func (s projectFlockKandangService) Closing(c *fiber.Ctx, id uint, req *validation.Closing) (*entity.ProjectFlockKandang, error) { if err := s.Validate.Struct(req); err != nil { return nil, err @@ -210,7 +358,7 @@ func (s projectFlockKandangService) Closing(c *fiber.Ctx, id uint, req *validati return nil, fiber.NewError(fiber.StatusConflict, "Kandang sudah closed") } if s.ExpenseRepo != nil && s.ApprovalSvc != nil { - unfinished, err := s.ExpenseRepo.CountUnfinishedByProjectFlockKandang(c.Context(), pfk.Id, func(appr *entity.Approval) bool { + unfinished, err := s.ExpenseRepo.CountUnfinishedByProjectFlockKandang(c.Context(), pfk.Id, pfk.KandangId, func(appr *entity.Approval) bool { return appr != nil && appr.StepNumber == uint16(utils.ExpenseStepSelesai) && appr.Action != nil && *appr.Action == entity.ApprovalActionApproved }) if err != nil { @@ -265,6 +413,38 @@ func (s projectFlockKandangService) Closing(c *fiber.Ctx, id uint, req *validati ); aerr != nil { return nil, aerr } + + // Jika semua kandang dalam project sudah ditutup, set approval project flock ke SELESAI. + pfks, ferr := s.Repository.GetByProjectFlockID(c.Context(), pfk.ProjectFlockId) + if ferr != nil { + return nil, ferr + } + allClosed := true + for _, item := range pfks { + if item.ClosedAt == nil { + allClosed = false + break + } + } + if allClosed { + latestPF, lerr := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowProjectFlock, pfk.ProjectFlockId, nil) + if lerr != nil { + return nil, lerr + } + if latestPF == nil || latestPF.StepNumber != uint16(utils.ProjectFlockStepSelesai) { + if _, aerr := s.ApprovalSvc.CreateApproval( + c.Context(), + utils.ApprovalWorkflowProjectFlock, + pfk.ProjectFlockId, + utils.ProjectFlockStepSelesai, + &closeAction, + actorID, + nil, + ); aerr != nil && !errors.Is(aerr, gorm.ErrDuplicatedKey) { + return nil, aerr + } + } + } } case "unclose": if pfk.ClosedAt == nil { diff --git a/internal/utils/constant.go b/internal/utils/constant.go index 01ec3ff1..5e1785f1 100644 --- a/internal/utils/constant.go +++ b/internal/utils/constant.go @@ -154,12 +154,14 @@ const ( ApprovalWorkflowProjectFlock approvalutils.ApprovalWorkflowKey = approvalutils.ApprovalWorkflowKey("PROJECT_FLOCKS") ProjectFlockStepPengajuan approvalutils.ApprovalStep = 1 ProjectFlockStepAktif approvalutils.ApprovalStep = 2 + ProjectFlockStepSelesai approvalutils.ApprovalStep = 3 ) // projectFlockApprovalSteps keeps the workflow step definitions for project flock approvals. var ProjectFlockApprovalSteps = map[approvalutils.ApprovalStep]string{ ProjectFlockStepPengajuan: "Pengajuan", ProjectFlockStepAktif: "Aktif", + ProjectFlockStepSelesai: "Selesai", } // -------------------------------------------------------------------