mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
feat(BE-281): unfinished uniformity and create project flock triger productwarehouse and add new filtering lookup
This commit is contained in:
+26
@@ -28,6 +28,8 @@ type ProductWarehouseRepository interface {
|
||||
IdExists(ctx context.Context, id uint) (bool, error)
|
||||
CleanupEmpty(ctx context.Context, affected map[uint]struct{}) error
|
||||
EnsureProductWarehouse(ctx context.Context, productID, warehouseID uint, projectFlockKandangID *uint, createdBy uint) (uint, error)
|
||||
GetByProductWarehouseAndProjectFlockKandang(ctx context.Context, productId, warehouseId, projectFlockKandangId uint) (*entity.ProductWarehouse, error)
|
||||
DeleteByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) error
|
||||
}
|
||||
|
||||
type ProductWarehouseRepositoryImpl struct {
|
||||
@@ -237,6 +239,30 @@ func (r *ProductWarehouseRepositoryImpl) EnsureProductWarehouse(
|
||||
return entity.Id, nil
|
||||
}
|
||||
|
||||
func (r *ProductWarehouseRepositoryImpl) GetByProductWarehouseAndProjectFlockKandang(
|
||||
ctx context.Context,
|
||||
productId uint,
|
||||
warehouseId uint,
|
||||
projectFlockKandangId uint,
|
||||
) (*entity.ProductWarehouse, error) {
|
||||
var productWarehouse entity.ProductWarehouse
|
||||
if err := r.DB().WithContext(ctx).
|
||||
Where("product_id = ? AND warehouse_id = ? AND project_flock_kandang_id = ?", productId, warehouseId, projectFlockKandangId).
|
||||
First(&productWarehouse).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &productWarehouse, nil
|
||||
}
|
||||
|
||||
func (r *ProductWarehouseRepositoryImpl) DeleteByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) error {
|
||||
if len(projectFlockKandangIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
return r.DB().WithContext(ctx).
|
||||
Where("project_flock_kandang_id IN ?", projectFlockKandangIDs).
|
||||
Delete(&entity.ProductWarehouse{}).Error
|
||||
}
|
||||
|
||||
func (r *ProductWarehouseRepositoryImpl) GetDetailByID(ctx context.Context, id uint) (*entity.ProductWarehouse, error) {
|
||||
var productWarehouse entity.ProductWarehouse
|
||||
err := r.DB().WithContext(ctx).
|
||||
|
||||
@@ -268,6 +268,7 @@ func (u *ProjectflockController) GetPeriodSummary(c *fiber.Ctx) error {
|
||||
func (u *ProjectflockController) LookupProjectFlockKandang(c *fiber.Ctx) error {
|
||||
projectFlockId := c.QueryInt("project_flock_id", 0)
|
||||
kandangId := c.QueryInt("kandang_id", 0)
|
||||
withPopulation := c.QueryBool("withpopulation", false)
|
||||
|
||||
if projectFlockId == 0 || kandangId == 0 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_id or kandang_id")
|
||||
@@ -280,6 +281,13 @@ func (u *ProjectflockController) LookupProjectFlockKandang(c *fiber.Ctx) error {
|
||||
|
||||
dtoResult := dto.ToProjectFlockKandangDTO(*result)
|
||||
dtoResult.AvailableQuantity = float64(availableStock)
|
||||
if withPopulation {
|
||||
population, err := u.ProjectflockService.GetProjectFlockKandangPopulation(c, result.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dtoResult.Population = &population
|
||||
}
|
||||
|
||||
if dtoResult.ProjectFlock != nil {
|
||||
for i := range dtoResult.ProjectFlock.Kandangs {
|
||||
|
||||
@@ -32,6 +32,7 @@ type ProjectFlockKandangDTO struct {
|
||||
Kandang *kandangDTO.KandangRelationDTO `json:"kandang,omitempty"`
|
||||
ProjectFlock *ProjectFlockWithPivotDTO `json:"project_flock,omitempty"`
|
||||
AvailableQuantity float64 `json:"available_quantity"`
|
||||
Population *float64 `json:"population,omitempty"`
|
||||
}
|
||||
|
||||
func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangDTO {
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
||||
rProjectBudget "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||
rProjectflock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||
rRecording "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
|
||||
|
||||
sProjectflock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/services"
|
||||
utils "gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
@@ -32,6 +33,8 @@ func (ProjectflockModule) RegisterRoutes(router fiber.Router, db *gorm.DB, valid
|
||||
nonstockRepo := rNonstock.NewNonstockRepository(db)
|
||||
projectflockRepo := rProjectflock.NewProjectflockRepository(db)
|
||||
projectflockKandangRepo := rProjectflock.NewProjectFlockKandangRepository(db)
|
||||
projectFlockPopulationRepo := rProjectflock.NewProjectFlockPopulationRepository(db)
|
||||
recordingRepo := rRecording.NewRecordingRepository(db)
|
||||
warehouseRepo := rWarehouse.NewWarehouseRepository(db)
|
||||
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
|
||||
projectBudgetRepo := rProjectBudget.NewProjectBudgetRepository(db)
|
||||
@@ -43,7 +46,7 @@ func (ProjectflockModule) RegisterRoutes(router fiber.Router, db *gorm.DB, valid
|
||||
panic(fmt.Sprintf("failed to register project flock approval workflow: %v", err))
|
||||
}
|
||||
|
||||
projectflockService := sProjectflock.NewProjectflockService(projectflockRepo, flockRepo, kandangRepo, projectflockKandangRepo, warehouseRepo, productWarehouseRepo, projectBudgetRepo, nonstockRepo, approvalService, validate)
|
||||
projectflockService := sProjectflock.NewProjectflockService(projectflockRepo, flockRepo, kandangRepo, projectflockKandangRepo, warehouseRepo, productWarehouseRepo, projectBudgetRepo, nonstockRepo, projectFlockPopulationRepo, recordingRepo, approvalService, validate)
|
||||
userService := sUser.NewUserService(userRepo, validate)
|
||||
|
||||
ProjectflockRoutes(router, userService, projectflockService)
|
||||
|
||||
+18
@@ -15,6 +15,7 @@ type ProjectFlockPopulationRepository interface {
|
||||
GetByProjectChickinIDAndProductWarehouseID(ctx context.Context, projectChickinID uint, productWarehouseID uint) ([]entity.ProjectFlockPopulation, error)
|
||||
GetByProjectFlockKandangIDAndProductWarehouseID(ctx context.Context, projectFlockKandangID uint, productWarehouseID uint) ([]entity.ProjectFlockPopulation, error)
|
||||
GetTotalQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error)
|
||||
GetAvailableQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error)
|
||||
|
||||
// subset of base repository methods used by services
|
||||
CreateOne(ctx context.Context, entity *entity.ProjectFlockPopulation, modifier func(*gorm.DB) *gorm.DB) error
|
||||
@@ -106,3 +107,20 @@ func (r *projectFlockPopulationRepositoryImpl) GetTotalQtyByProjectFlockKandangI
|
||||
}
|
||||
return total, nil
|
||||
}
|
||||
|
||||
func (r *projectFlockPopulationRepositoryImpl) GetAvailableQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error) {
|
||||
var total float64
|
||||
err := r.DB().WithContext(ctx).
|
||||
Table("project_flock_populations").
|
||||
Select("COALESCE(SUM(total_qty - total_used_qty), 0) AS total_qty").
|
||||
Joins("JOIN project_chickins ON project_chickins.id = project_flock_populations.project_chickin_id").
|
||||
Where("project_chickins.project_flock_kandang_id = ?", projectFlockKandangID).
|
||||
Scan(&total).Error
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if total < 0 {
|
||||
total = 0
|
||||
}
|
||||
return total, nil
|
||||
}
|
||||
|
||||
+15
@@ -27,6 +27,7 @@ type ProjectFlockKandangRepository interface {
|
||||
MaxPeriodByBaseName(ctx context.Context, baseName string) (int, error)
|
||||
ProjectPeriodsByProjectIDs(ctx context.Context, projectIDs []uint) (map[uint]int, error)
|
||||
HasOpenNewerPeriod(ctx context.Context, kandangID uint, currentPeriod int, excludeID *uint) (bool, error)
|
||||
ListIDsByProjectAndKandang(ctx context.Context, projectFlockID uint, kandangIDs []uint) ([]uint, error)
|
||||
WithTx(tx *gorm.DB) ProjectFlockKandangRepository
|
||||
DB() *gorm.DB
|
||||
IdExists(ctx context.Context, id uint) (bool, error)
|
||||
@@ -89,6 +90,20 @@ func (r *projectFlockKandangRepositoryImpl) GetByProjectFlockID(ctx context.Cont
|
||||
return records, nil
|
||||
}
|
||||
|
||||
func (r *projectFlockKandangRepositoryImpl) ListIDsByProjectAndKandang(ctx context.Context, projectFlockID uint, kandangIDs []uint) ([]uint, error) {
|
||||
if len(kandangIDs) == 0 {
|
||||
return []uint{}, nil
|
||||
}
|
||||
var ids []uint
|
||||
if err := r.db.WithContext(ctx).
|
||||
Model(&entity.ProjectFlockKandang{}).
|
||||
Where("project_flock_id = ? AND kandang_id IN ?", projectFlockID, kandangIDs).
|
||||
Pluck("id", &ids).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
func (r *projectFlockKandangRepositoryImpl) GetAllWithFilters(ctx context.Context, offset int, limit int, params interface{}) ([]entity.ProjectFlockKandang, int64, error) {
|
||||
var records []entity.ProjectFlockKandang
|
||||
var total int64
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||
pfutils "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/utils"
|
||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/validations"
|
||||
recordingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
|
||||
utils "gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
|
||||
|
||||
@@ -37,6 +38,7 @@ type ProjectflockService interface {
|
||||
GetAvailableDocQuantity(ctx *fiber.Ctx, kandangID uint) (float64, error)
|
||||
DeleteOne(ctx *fiber.Ctx, id uint) error
|
||||
GetProjectFlockKandangByProjectAndKandang(ctx *fiber.Ctx, projectFlockID uint, kandangID uint) (*entity.ProjectFlockKandang, float64, error)
|
||||
GetProjectFlockKandangPopulation(ctx *fiber.Ctx, projectFlockKandangID uint) (float64, error)
|
||||
GetPeriodSummary(ctx *fiber.Ctx, locationID uint) ([]KandangPeriodSummary, error)
|
||||
GetProjectPeriods(ctx *fiber.Ctx, projectIDs []uint) (map[uint]int, error)
|
||||
Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.ProjectFlock, error)
|
||||
@@ -54,6 +56,8 @@ type projectflockService struct {
|
||||
ProductWarehouseRepo productWarehouseRepository.ProductWarehouseRepository
|
||||
ProjectBudgetRepo projectBudgetRepository.ProjectBudgetRepository
|
||||
PivotRepo repository.ProjectFlockKandangRepository
|
||||
PopulationRepo repository.ProjectFlockPopulationRepository
|
||||
RecordingRepo recordingRepo.RecordingRepository
|
||||
ApprovalSvc commonSvc.ApprovalService
|
||||
approvalWorkflow approvalutils.ApprovalWorkflowKey
|
||||
}
|
||||
@@ -73,6 +77,8 @@ func NewProjectflockService(
|
||||
productWarehouseRepo productWarehouseRepository.ProductWarehouseRepository,
|
||||
projectBudgetRepo projectBudgetRepository.ProjectBudgetRepository,
|
||||
nonstockRepo nonstockRepository.NonstockRepository,
|
||||
populationRepo repository.ProjectFlockPopulationRepository,
|
||||
recordingRepo recordingRepo.RecordingRepository,
|
||||
approvalSvc commonSvc.ApprovalService,
|
||||
validate *validator.Validate,
|
||||
|
||||
@@ -86,7 +92,10 @@ func NewProjectflockService(
|
||||
NonstockRepo: nonstockRepo,
|
||||
WarehouseRepo: warehouseRepo,
|
||||
ProductWarehouseRepo: productWarehouseRepo,
|
||||
ProjectBudgetRepo: projectBudgetRepo,
|
||||
PivotRepo: pivotRepo,
|
||||
PopulationRepo: populationRepo,
|
||||
RecordingRepo: recordingRepo,
|
||||
ApprovalSvc: approvalSvc,
|
||||
approvalWorkflow: utils.ApprovalWorkflowProjectFlock,
|
||||
}
|
||||
@@ -417,6 +426,34 @@ func (s projectflockService) GetProjectFlockKandangByProjectAndKandang(ctx *fibe
|
||||
return pfk, availableQuantity, nil
|
||||
}
|
||||
|
||||
func (s projectflockService) GetProjectFlockKandangPopulation(ctx *fiber.Ctx, projectFlockKandangID uint) (float64, error) {
|
||||
if s.PopulationRepo == nil {
|
||||
return 0, fiber.NewError(fiber.StatusInternalServerError, "Project flock population repository is not configured")
|
||||
}
|
||||
if projectFlockKandangID == 0 {
|
||||
return 0, fiber.NewError(fiber.StatusBadRequest, "project_flock_kandang_id is required")
|
||||
}
|
||||
|
||||
if s.RecordingRepo != nil {
|
||||
latest, err := s.RecordingRepo.GetLatestByProjectFlockKandangID(ctx.Context(), projectFlockKandangID)
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to fetch latest recording for project flock kandang %d: %+v", projectFlockKandangID, err)
|
||||
return 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandang population")
|
||||
}
|
||||
if latest != nil && latest.TotalChickQty != nil && *latest.TotalChickQty > 0 {
|
||||
return *latest.TotalChickQty, nil
|
||||
}
|
||||
}
|
||||
|
||||
total, err := s.PopulationRepo.GetAvailableQtyByProjectFlockKandangID(ctx.Context(), projectFlockKandangID)
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to fetch project flock kandang population %d: %+v", projectFlockKandangID, err)
|
||||
return 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandang population")
|
||||
}
|
||||
|
||||
return total, nil
|
||||
}
|
||||
|
||||
func (s projectflockService) GetProjectFlockKandangByParams(ctx *fiber.Ctx, idStr string, projectFlockIdStr string, kandangIdStr string) (*entity.ProjectFlockKandang, float64, error) {
|
||||
idStr = strings.TrimSpace(idStr)
|
||||
projectFlockIdStr = strings.TrimSpace(projectFlockIdStr)
|
||||
@@ -793,6 +830,9 @@ func (s projectflockService) attachKandangs(ctx context.Context, dbTransaction *
|
||||
}
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to persist project flock history")
|
||||
}
|
||||
if err := s.ensureProjectFlockKandangProductWarehouses(ctx, dbTransaction, records); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -818,6 +858,23 @@ func (s projectflockService) detachKandangs(ctx context.Context, dbTransaction *
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Tidak dapat melepas kandang karena sudah memiliki recording: %s", strings.Join(names, ", ")))
|
||||
}
|
||||
|
||||
pfkIDs, err := s.pivotRepoWithTx(dbTransaction).ListIDsByProjectAndKandang(ctx, projectFlockID, kandangIDs)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to load project flock kandang ids")
|
||||
}
|
||||
|
||||
if len(pfkIDs) > 0 {
|
||||
pwRepo := s.ProductWarehouseRepo
|
||||
if dbTransaction != nil {
|
||||
pwRepo = productWarehouseRepository.NewProductWarehouseRepository(dbTransaction)
|
||||
} else if pwRepo == nil {
|
||||
pwRepo = productWarehouseRepository.NewProductWarehouseRepository(s.Repository.DB())
|
||||
}
|
||||
if err := pwRepo.DeleteByProjectFlockKandangIDs(ctx, pfkIDs); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to remove product warehouses for project flock kandang")
|
||||
}
|
||||
}
|
||||
|
||||
if resetStatus {
|
||||
if err := s.kandangRepoWithTx(dbTransaction).UpdateStatusByIDs(ctx, kandangIDs, utils.KandangStatusNonActive); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update kandangs status")
|
||||
@@ -854,6 +911,78 @@ func (s projectflockService) kandangRepoWithTx(tx *gorm.DB) kandangRepository.Ka
|
||||
return kandangRepository.NewKandangRepository(s.Repository.DB())
|
||||
}
|
||||
|
||||
func (s projectflockService) ensureProjectFlockKandangProductWarehouses(ctx context.Context, dbTransaction *gorm.DB, records []*entity.ProjectFlockKandang) error {
|
||||
if len(records) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
pwRepo := s.ProductWarehouseRepo
|
||||
if dbTransaction != nil {
|
||||
pwRepo = productWarehouseRepository.NewProductWarehouseRepository(dbTransaction)
|
||||
} else if pwRepo == nil {
|
||||
pwRepo = productWarehouseRepository.NewProductWarehouseRepository(s.Repository.DB())
|
||||
}
|
||||
|
||||
warehouseRepo := s.WarehouseRepo
|
||||
if dbTransaction != nil {
|
||||
warehouseRepo = warehouseRepository.NewWarehouseRepository(dbTransaction)
|
||||
} else if warehouseRepo == nil {
|
||||
warehouseRepo = warehouseRepository.NewWarehouseRepository(s.Repository.DB())
|
||||
}
|
||||
|
||||
flags := []utils.FlagType{
|
||||
utils.FlagAyamAfkir,
|
||||
utils.FlagAyamCulling,
|
||||
utils.FlagAyamMati,
|
||||
utils.FlagTelurPecah,
|
||||
utils.FlagTelurUtuh,
|
||||
}
|
||||
|
||||
productIDs := make(map[utils.FlagType]uint, len(flags))
|
||||
for _, flag := range flags {
|
||||
product, err := pwRepo.GetFirstProductByFlag(ctx, string(flag))
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Product untuk flag %s tidak ditemukan", flag))
|
||||
}
|
||||
return err
|
||||
}
|
||||
productIDs[flag] = product.Id
|
||||
}
|
||||
|
||||
for _, record := range records {
|
||||
if record == nil || record.Id == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
warehouse, err := warehouseRepo.GetByKandangID(ctx, record.KandangId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, flag := range flags {
|
||||
productID := productIDs[flag]
|
||||
if _, err := pwRepo.GetByProductWarehouseAndProjectFlockKandang(ctx, productID, warehouse.Id, record.Id); err == nil {
|
||||
continue
|
||||
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
}
|
||||
|
||||
newPW := entity.ProductWarehouse{
|
||||
ProductId: productID,
|
||||
WarehouseId: warehouse.Id,
|
||||
ProjectFlockKandangId: &record.Id,
|
||||
Quantity: 0,
|
||||
}
|
||||
if err := pwRepo.CreateOne(ctx, &newPW, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s projectflockService) Resubmit(c *fiber.Ctx, req *validation.Resubmit, id uint) (*entity.ProjectFlock, error) {
|
||||
if err := s.Validate.Struct(req); err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -17,6 +17,7 @@ type RecordingRepository interface {
|
||||
repository.BaseRepository[entity.Recording]
|
||||
|
||||
WithRelations(db *gorm.DB) *gorm.DB
|
||||
GetLatestByProjectFlockKandangID(ctx context.Context, projectFlockKandangId uint) (*entity.Recording, error)
|
||||
GenerateNextDay(tx *gorm.DB, projectFlockKandangId uint) (int, error)
|
||||
|
||||
CreateBodyWeights(tx *gorm.DB, bodyWeights []entity.RecordingBW) error
|
||||
@@ -81,6 +82,27 @@ func (r *RecordingRepositoryImpl) WithRelations(db *gorm.DB) *gorm.DB {
|
||||
Preload("Eggs.ProductWarehouse.Warehouse")
|
||||
}
|
||||
|
||||
func (r *RecordingRepositoryImpl) GetLatestByProjectFlockKandangID(ctx context.Context, projectFlockKandangId uint) (*entity.Recording, error) {
|
||||
if projectFlockKandangId == 0 {
|
||||
return nil, errors.New("project_flock_kandang_id is required")
|
||||
}
|
||||
|
||||
var record entity.Recording
|
||||
err := r.DB().WithContext(ctx).
|
||||
Where("project_flock_kandangs_id = ?", projectFlockKandangId).
|
||||
Order("record_datetime DESC").
|
||||
Order("created_at DESC").
|
||||
Limit(1).
|
||||
Find(&record).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) || record.Id == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &record, nil
|
||||
}
|
||||
|
||||
func (r *RecordingRepositoryImpl) GenerateNextDay(tx *gorm.DB, projectFlockKandangId uint) (int, error) {
|
||||
var days []int
|
||||
if err := tx.Model(&entity.Recording{}).
|
||||
|
||||
@@ -8,10 +8,11 @@ import (
|
||||
"gorm.io/gorm"
|
||||
|
||||
chickins "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins"
|
||||
projectFlockKandangs "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs"
|
||||
projectflocks "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks"
|
||||
recordings "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings"
|
||||
transferLayings "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings"
|
||||
projectFlockKandangs "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs"
|
||||
uniformitys "gitlab.com/mbugroup/lti-api.git/internal/modules/production/uniformities"
|
||||
// MODULE IMPORTS
|
||||
)
|
||||
|
||||
@@ -24,8 +25,9 @@ func RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Valida
|
||||
chickins.ChickinModule{},
|
||||
transferLayings.TransferLayingModule{},
|
||||
projectFlockKandangs.ProjectFlockKandangModule{},
|
||||
uniformitys.UniformityModule{},
|
||||
// MODULE REGISTRY
|
||||
}
|
||||
}
|
||||
|
||||
for _, m := range allModules {
|
||||
m.RegisterRoutes(group, db, validate)
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/modules/production/uniformities/dto"
|
||||
service "gitlab.com/mbugroup/lti-api.git/internal/modules/production/uniformities/services"
|
||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/uniformities/validations"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/response"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
)
|
||||
|
||||
type UniformityController struct {
|
||||
UniformityService service.UniformityService
|
||||
}
|
||||
|
||||
func NewUniformityController(uniformityService service.UniformityService) *UniformityController {
|
||||
return &UniformityController{
|
||||
UniformityService: uniformityService,
|
||||
}
|
||||
}
|
||||
|
||||
func (u *UniformityController) GetAll(c *fiber.Ctx) error {
|
||||
query, err := validation.ParseQuery(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
result, totalResults, err := u.UniformityService.GetAll(c, query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).
|
||||
JSON(response.SuccessWithPaginate[dto.UniformityListDTO]{
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: "Get all production uniformities successfully",
|
||||
Meta: response.Meta{
|
||||
Page: query.Page,
|
||||
Limit: query.Limit,
|
||||
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
|
||||
TotalResults: totalResults,
|
||||
Filters: fiber.Map{
|
||||
"location_id": "",
|
||||
"project_flock_id": "",
|
||||
"status": "Pengajuan",
|
||||
},
|
||||
},
|
||||
Data: dto.ToUniformityListDTOs(result),
|
||||
})
|
||||
}
|
||||
|
||||
func (u *UniformityController) GetOne(c *fiber.Ctx) error {
|
||||
id, err := validation.ParseIDParam(c, "id")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
result, err := u.UniformityService.GetOne(c, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
withDetails := c.QueryBool("with_details", false)
|
||||
calculation := service.UniformityCalculation{}
|
||||
var document *entity.Document
|
||||
var meanWeight float64
|
||||
if result.MeanUp > 0 {
|
||||
meanWeight = math.Round(result.MeanUp / 1.10)
|
||||
}
|
||||
if withDetails {
|
||||
var err error
|
||||
calculation, document, err = u.UniformityService.CalculateUniformityFromDocument(c, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
calculation = service.UniformityCalculation{
|
||||
ChickQtyOfWeight: result.ChickQtyOfWeight,
|
||||
MeanWeight: meanWeight,
|
||||
MeanDown: result.MeanDown,
|
||||
MeanUp: result.MeanUp,
|
||||
UniformQty: result.UniformQty,
|
||||
OutsideQty: result.NotUniformQty,
|
||||
Uniformity: result.Uniformity,
|
||||
Cv: result.Cv,
|
||||
}
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).
|
||||
JSON(response.Success{
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: "Get production uniformity successfully",
|
||||
Data: dto.ToUniformityDetailDTO(*result, calculation, document),
|
||||
})
|
||||
}
|
||||
|
||||
func (u *UniformityController) CreateOne(c *fiber.Ctx) error {
|
||||
req, file, err := validation.ParseCreate(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rows, err := u.UniformityService.ParseBodyWeightExcel(c, file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
calculation, err := u.UniformityService.ComputeUniformity(rows)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
result, err := u.UniformityService.CreateOne(c, req, file, rows)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
document := dto.NewDocumentForResponse(file.Filename)
|
||||
|
||||
return c.Status(fiber.StatusCreated).
|
||||
JSON(response.Success{
|
||||
Code: fiber.StatusCreated,
|
||||
Status: "success",
|
||||
Message: "Create uniformity successfully",
|
||||
Data: dto.ToUniformityDetailDTO(*result, calculation, document),
|
||||
})
|
||||
}
|
||||
|
||||
func (u *UniformityController) UploadBodyWeightExcel(c *fiber.Ctx) error {
|
||||
files, err := validation.ParseUploadFiles(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rows, err := u.UniformityService.ParseBodyWeightExcel(c, files[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
calculation, err := u.UniformityService.ComputeUniformity(rows)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).
|
||||
JSON(response.Success{
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: "Uniformity verified successfully",
|
||||
Data: dto.ToUniformityVerificationDTO(calculation),
|
||||
})
|
||||
}
|
||||
|
||||
func (u *UniformityController) UpdateOne(c *fiber.Ctx) error {
|
||||
id, err := validation.ParseIDParam(c, "id")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req, file, err := validation.ParseUpdate(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var rows []service.BodyWeightExcelRow
|
||||
if file != nil {
|
||||
parsed, err := u.UniformityService.ParseBodyWeightExcel(c, file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rows = parsed
|
||||
}
|
||||
|
||||
result, err := u.UniformityService.UpdateOne(c, req, id, file, rows)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
calculation, document, err := u.UniformityService.CalculateUniformityFromDocument(c, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).
|
||||
JSON(response.Success{
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: "Update uniformity successfully",
|
||||
Data: dto.ToUniformityDetailDTO(*result, calculation, document),
|
||||
})
|
||||
}
|
||||
|
||||
func (u *UniformityController) DeleteOne(c *fiber.Ctx) error {
|
||||
id, err := validation.ParseIDParam(c, "id")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := u.UniformityService.DeleteOne(c, id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).
|
||||
JSON(response.Common{
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: "Delete uniformity successfully",
|
||||
})
|
||||
}
|
||||
|
||||
func (u *UniformityController) Approve(c *fiber.Ctx) error {
|
||||
req, err := validation.ParseApprove(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
results, err := u.UniformityService.Approval(c, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var (
|
||||
data interface{}
|
||||
message = "Submit uniformity approvals successfully"
|
||||
)
|
||||
|
||||
if len(results) == 1 {
|
||||
message = "Submit uniformity approval successfully"
|
||||
data = dto.ToUniformityListDTOs(results)[0]
|
||||
} else {
|
||||
data = dto.ToUniformityListDTOs(results)
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).
|
||||
JSON(response.Success{
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: message,
|
||||
Data: data,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto"
|
||||
service "gitlab.com/mbugroup/lti-api.git/internal/modules/production/uniformities/services"
|
||||
)
|
||||
|
||||
type UniformitySamplingDTO struct {
|
||||
ChickQtyOfWeight float64 `json:"chick_qty_of_weight"`
|
||||
MeanWeight float64 `json:"mean_weight"`
|
||||
MeanDown float64 `json:"mean_down"`
|
||||
MeanUp float64 `json:"mean_up"`
|
||||
}
|
||||
|
||||
type UniformityResultDTO struct {
|
||||
UniformQty float64 `json:"uniform_qty"`
|
||||
OutsideQty float64 `json:"outside_qty"`
|
||||
Uniformity float64 `json:"uniformity"`
|
||||
Cv float64 `json:"cv"`
|
||||
}
|
||||
|
||||
type UniformityDetailItemDTO struct {
|
||||
Id int `json:"id"`
|
||||
Weight float64 `json:"weight"`
|
||||
Range string `json:"range"`
|
||||
}
|
||||
|
||||
type UniformityVerificationDTO struct {
|
||||
Sampling UniformitySamplingDTO `json:"sampling"`
|
||||
Result UniformityResultDTO `json:"result"`
|
||||
UniformityDetails []UniformityDetailItemDTO `json:"uniformity_details"`
|
||||
}
|
||||
|
||||
type UniformityInfoDTO struct {
|
||||
Tanggal string `json:"tanggal"`
|
||||
LokasiFarm string `json:"lokasi_farm"`
|
||||
ProjectFlock string `json:"project_flock"`
|
||||
Kandang string `json:"kandang"`
|
||||
FileName string `json:"file_name"`
|
||||
}
|
||||
|
||||
type UniformityDetailDTO struct {
|
||||
Id uint `json:"id"`
|
||||
InfoUmum UniformityInfoDTO `json:"info_umum"`
|
||||
Sampling UniformitySamplingDTO `json:"sampling"`
|
||||
Result UniformityResultDTO `json:"result"`
|
||||
UniformityDetails []UniformityDetailItemDTO `json:"uniformity_details"`
|
||||
}
|
||||
|
||||
type UniformityListDTO struct {
|
||||
Id uint `json:"id"`
|
||||
ProjectFlockKandangId uint `json:"project_flock_kandang_id"`
|
||||
LocationName string `json:"location_name"`
|
||||
FlockName string `json:"flock_name"`
|
||||
KandangName string `json:"kandang_name"`
|
||||
AppliedAt *time.Time `json:"applied_at"`
|
||||
Week int `json:"week"`
|
||||
Status string `json:"status"`
|
||||
Uniformity float64 `json:"uniformity"`
|
||||
Cv float64 `json:"cv"`
|
||||
ChickQtyOfWeight float64 `json:"chick_qty_of_weight"`
|
||||
UniformQty float64 `json:"uniform_qty"`
|
||||
MeanUp float64 `json:"mean_up"`
|
||||
MeanDown float64 `json:"mean_down"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
CreatedBy uint `json:"created_by"`
|
||||
LatestApproval *approvalDTO.ApprovalRelationDTO `json:"latest_approval"`
|
||||
}
|
||||
|
||||
func NewDocumentForResponse(name string) *entity.Document {
|
||||
if name == "" {
|
||||
return nil
|
||||
}
|
||||
return &entity.Document{Name: name}
|
||||
}
|
||||
|
||||
func ToUniformityVerificationDTO(calc service.UniformityCalculation) UniformityVerificationDTO {
|
||||
return UniformityVerificationDTO{
|
||||
Sampling: toUniformitySamplingDTO(calc),
|
||||
Result: toUniformityResultDTO(calc),
|
||||
UniformityDetails: toUniformityDetailItemsDTO(calc),
|
||||
}
|
||||
}
|
||||
|
||||
func ToUniformityDetailDTO(
|
||||
entityData entity.ProjectFlockKandangUniformity,
|
||||
calc service.UniformityCalculation,
|
||||
document *entity.Document,
|
||||
) UniformityDetailDTO {
|
||||
info := UniformityInfoDTO{
|
||||
Tanggal: formatUniformityDate(entityData.UniformDate),
|
||||
LokasiFarm: resolveLocationName(entityData.ProjectFlockKandang),
|
||||
ProjectFlock: resolveProjectFlockName(entityData.ProjectFlockKandang),
|
||||
Kandang: resolveKandangName(entityData.ProjectFlockKandang),
|
||||
FileName: "",
|
||||
}
|
||||
if document != nil {
|
||||
info.FileName = document.Name
|
||||
}
|
||||
|
||||
return UniformityDetailDTO{
|
||||
Id: entityData.Id,
|
||||
InfoUmum: info,
|
||||
Sampling: toUniformitySamplingDTO(calc),
|
||||
Result: toUniformityResultDTO(calc),
|
||||
UniformityDetails: toUniformityDetailItemsDTO(calc),
|
||||
}
|
||||
}
|
||||
|
||||
func ToUniformityListDTOs(items []entity.ProjectFlockKandangUniformity) []UniformityListDTO {
|
||||
result := make([]UniformityListDTO, len(items))
|
||||
for i, item := range items {
|
||||
var latestApproval *approvalDTO.ApprovalRelationDTO
|
||||
status := "Pengajuan"
|
||||
if item.LatestApproval != nil {
|
||||
mapped := approvalDTO.ToApprovalDTO(*item.LatestApproval)
|
||||
latestApproval = &mapped
|
||||
if mapped.StepName != "" {
|
||||
status = mapped.StepName
|
||||
}
|
||||
}
|
||||
|
||||
result[i] = UniformityListDTO{
|
||||
Id: item.Id,
|
||||
ProjectFlockKandangId: item.ProjectFlockKandangId,
|
||||
LocationName: resolveLocationName(item.ProjectFlockKandang),
|
||||
FlockName: resolveProjectFlockName(item.ProjectFlockKandang),
|
||||
KandangName: resolveKandangName(item.ProjectFlockKandang),
|
||||
AppliedAt: item.UniformDate,
|
||||
Week: item.Week,
|
||||
Status: status,
|
||||
Uniformity: item.Uniformity,
|
||||
Cv: item.Cv,
|
||||
ChickQtyOfWeight: item.ChickQtyOfWeight,
|
||||
UniformQty: item.UniformQty,
|
||||
MeanUp: item.MeanUp,
|
||||
MeanDown: item.MeanDown,
|
||||
CreatedAt: item.CreatedAt,
|
||||
CreatedBy: item.CreatedBy,
|
||||
LatestApproval: latestApproval,
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func toUniformitySamplingDTO(calc service.UniformityCalculation) UniformitySamplingDTO {
|
||||
return UniformitySamplingDTO{
|
||||
ChickQtyOfWeight: calc.ChickQtyOfWeight,
|
||||
MeanWeight: calc.MeanWeight,
|
||||
MeanDown: calc.MeanDown,
|
||||
MeanUp: calc.MeanUp,
|
||||
}
|
||||
}
|
||||
|
||||
func toUniformityResultDTO(calc service.UniformityCalculation) UniformityResultDTO {
|
||||
return UniformityResultDTO{
|
||||
UniformQty: calc.UniformQty,
|
||||
OutsideQty: calc.OutsideQty,
|
||||
Uniformity: calc.Uniformity,
|
||||
Cv: calc.Cv,
|
||||
}
|
||||
}
|
||||
|
||||
func toUniformityDetailItemsDTO(calc service.UniformityCalculation) []UniformityDetailItemDTO {
|
||||
result := make([]UniformityDetailItemDTO, len(calc.Details))
|
||||
for i, item := range calc.Details {
|
||||
result[i] = UniformityDetailItemDTO{
|
||||
Id: item.Id,
|
||||
Weight: item.Weight,
|
||||
Range: item.Range,
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func resolveLocationName(pfk entity.ProjectFlockKandang) string {
|
||||
if pfk.Kandang.Id != 0 && pfk.Kandang.Location.Id != 0 {
|
||||
return pfk.Kandang.Location.Name
|
||||
}
|
||||
if pfk.ProjectFlock.Id != 0 && pfk.ProjectFlock.Location.Id != 0 {
|
||||
return pfk.ProjectFlock.Location.Name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func resolveProjectFlockName(pfk entity.ProjectFlockKandang) string {
|
||||
if pfk.ProjectFlock.Id != 0 {
|
||||
return pfk.ProjectFlock.FlockName
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func resolveKandangName(pfk entity.ProjectFlockKandang) string {
|
||||
if pfk.Kandang.Id != 0 {
|
||||
return pfk.Kandang.Name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func formatUniformityDate(date *time.Time) string {
|
||||
if date == nil || date.IsZero() {
|
||||
return ""
|
||||
}
|
||||
return date.Format("2006-01-02")
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package uniformitys
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"gorm.io/gorm"
|
||||
|
||||
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||
rUniformity "gitlab.com/mbugroup/lti-api.git/internal/modules/production/uniformities/repositories"
|
||||
sUniformity "gitlab.com/mbugroup/lti-api.git/internal/modules/production/uniformities/services"
|
||||
|
||||
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
||||
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
)
|
||||
|
||||
type UniformityModule struct{}
|
||||
|
||||
func (UniformityModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
||||
uniformityRepo := rUniformity.NewUniformityRepository(db)
|
||||
documentRepo := commonRepo.NewDocumentRepository(db)
|
||||
approvalRepo := commonRepo.NewApprovalRepository(db)
|
||||
userRepo := rUser.NewUserRepository(db)
|
||||
|
||||
documentSvc, err := commonSvc.NewDocumentServiceFromConfig(context.Background(), documentRepo)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("failed to create document service: %v", err))
|
||||
}
|
||||
|
||||
approvalSvc := commonSvc.NewApprovalService(approvalRepo)
|
||||
if err := approvalSvc.RegisterWorkflowSteps(utils.ApprovalWorkflowUniformity, utils.UniformityApprovalSteps); err != nil {
|
||||
panic(fmt.Sprintf("failed to register uniformity approval workflow: %v", err))
|
||||
}
|
||||
|
||||
uniformityService := sUniformity.NewUniformityService(uniformityRepo, documentSvc, approvalRepo, approvalSvc, validate)
|
||||
userService := sUser.NewUserService(userRepo, validate)
|
||||
|
||||
UniformityRoutes(router, userService, uniformityService)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type UniformityRepository interface {
|
||||
repository.BaseRepository[entity.ProjectFlockKandangUniformity]
|
||||
}
|
||||
|
||||
type UniformityRepositoryImpl struct {
|
||||
*repository.BaseRepositoryImpl[entity.ProjectFlockKandangUniformity]
|
||||
}
|
||||
|
||||
func NewUniformityRepository(db *gorm.DB) UniformityRepository {
|
||||
return &UniformityRepositoryImpl{
|
||||
BaseRepositoryImpl: repository.NewBaseRepository[entity.ProjectFlockKandangUniformity](db),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package uniformitys
|
||||
|
||||
import (
|
||||
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/production/uniformities/controllers"
|
||||
uniformity "gitlab.com/mbugroup/lti-api.git/internal/modules/production/uniformities/services"
|
||||
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func UniformityRoutes(v1 fiber.Router, u user.UserService, s uniformity.UniformityService) {
|
||||
ctrl := controller.NewUniformityController(s)
|
||||
|
||||
route := v1.Group("/uniformities")
|
||||
|
||||
// route.Get("/", m.Auth(u), ctrl.GetAll)
|
||||
// route.Post("/", m.Auth(u), ctrl.CreateOne)
|
||||
// route.Get("/:id", m.Auth(u), ctrl.GetOne)
|
||||
// route.Patch("/:id", m.Auth(u), ctrl.UpdateOne)
|
||||
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
|
||||
|
||||
route.Get("/", ctrl.GetAll)
|
||||
route.Post("/", ctrl.CreateOne)
|
||||
route.Post("/verify", ctrl.UploadBodyWeightExcel)
|
||||
route.Post("/approvals", ctrl.Approve)
|
||||
route.Get("/:id", ctrl.GetOne)
|
||||
route.Patch("/:id", ctrl.UpdateOne)
|
||||
route.Delete("/:id", ctrl.DeleteOne)
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/xuri/excelize/v2"
|
||||
)
|
||||
|
||||
type BodyWeightExcelRow struct {
|
||||
No int `json:"no"`
|
||||
Weight float64 `json:"weight"`
|
||||
Range string `json:"range,omitempty"`
|
||||
}
|
||||
|
||||
func (s uniformityService) ParseBodyWeightExcel(_ *fiber.Ctx, file *multipart.FileHeader) ([]BodyWeightExcelRow, error) {
|
||||
if file == nil {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "file is required")
|
||||
}
|
||||
|
||||
reader, err := file.Open()
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "failed to open file")
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
rows, err := parseBodyWeightExcelReader(reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
func parseBodyWeightExcelReader(reader io.Reader) ([]BodyWeightExcelRow, error) {
|
||||
xlsx, err := excelize.OpenReader(reader)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "failed to read excel file")
|
||||
}
|
||||
defer func() {
|
||||
_ = xlsx.Close()
|
||||
}()
|
||||
|
||||
sheets := xlsx.GetSheetList()
|
||||
if len(sheets) == 0 {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "no sheets found in file")
|
||||
}
|
||||
|
||||
rows, err := xlsx.GetRows(sheets[0], excelize.Options{RawCellValue: true})
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "failed to read sheet rows")
|
||||
}
|
||||
|
||||
return parseBodyWeightRows(rows)
|
||||
}
|
||||
|
||||
func parseBodyWeightRows(rows [][]string) ([]BodyWeightExcelRow, error) {
|
||||
headerRowIdx, noCol, bwCol, rangeCol := findBodyWeightHeader(rows)
|
||||
if headerRowIdx < 0 || bwCol < 0 {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "header BW not found")
|
||||
}
|
||||
|
||||
result := make([]BodyWeightExcelRow, 0)
|
||||
lastNo := 0
|
||||
|
||||
for i := headerRowIdx + 1; i < len(rows); i++ {
|
||||
row := rows[i]
|
||||
weightStr := cellAt(row, bwCol)
|
||||
weightVal, ok := parseNumber(weightStr)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
noVal := 0
|
||||
if noCol >= 0 {
|
||||
if parsed, ok := parseNumber(cellAt(row, noCol)); ok {
|
||||
noVal = int(parsed)
|
||||
}
|
||||
}
|
||||
if noVal <= 0 {
|
||||
noVal = lastNo + 1
|
||||
}
|
||||
if noVal > lastNo {
|
||||
lastNo = noVal
|
||||
}
|
||||
|
||||
rangeVal := ""
|
||||
if rangeCol >= 0 {
|
||||
rangeVal = strings.TrimSpace(cellAt(row, rangeCol))
|
||||
}
|
||||
|
||||
rowPayload := BodyWeightExcelRow{
|
||||
No: noVal,
|
||||
Weight: weightVal,
|
||||
Range: rangeVal,
|
||||
}
|
||||
if rowPayload.No <= 0 || rowPayload.Weight <= 0 {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "invalid body weight row data")
|
||||
}
|
||||
|
||||
result = append(result, rowPayload)
|
||||
}
|
||||
|
||||
if len(result) == 0 {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "no body weight data found")
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func findBodyWeightHeader(rows [][]string) (rowIdx int, noCol int, bwCol int, rangeCol int) {
|
||||
rowIdx = -1
|
||||
noCol = -1
|
||||
bwCol = -1
|
||||
rangeCol = -1
|
||||
|
||||
for i, row := range rows {
|
||||
tempNo := -1
|
||||
tempBW := -1
|
||||
tempRange := -1
|
||||
for j, cell := range row {
|
||||
label := normalizeHeader(cell)
|
||||
switch label {
|
||||
case "no":
|
||||
tempNo = j
|
||||
case "bw":
|
||||
tempBW = j
|
||||
case "outsiderange":
|
||||
tempRange = j
|
||||
default:
|
||||
if strings.HasPrefix(label, "bw") {
|
||||
tempBW = j
|
||||
} else if strings.HasPrefix(label, "no") {
|
||||
tempNo = j
|
||||
} else if strings.Contains(label, "range") {
|
||||
tempRange = j
|
||||
}
|
||||
}
|
||||
}
|
||||
if tempBW >= 0 {
|
||||
rowIdx = i
|
||||
bwCol = tempBW
|
||||
noCol = tempNo
|
||||
rangeCol = tempRange
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return rowIdx, noCol, bwCol, rangeCol
|
||||
}
|
||||
|
||||
func cellAt(row []string, idx int) string {
|
||||
if idx < 0 || idx >= len(row) {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(row[idx])
|
||||
}
|
||||
|
||||
func normalizeHeader(value string) string {
|
||||
trimmed := strings.ToLower(strings.TrimSpace(value))
|
||||
if trimmed == "" {
|
||||
return ""
|
||||
}
|
||||
var b strings.Builder
|
||||
for _, r := range trimmed {
|
||||
if r >= 'a' && r <= 'z' {
|
||||
b.WriteRune(r)
|
||||
}
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func parseNumber(value string) (float64, bool) {
|
||||
trimmed := strings.TrimSpace(value)
|
||||
if trimmed == "" {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
if strings.Contains(trimmed, ",") {
|
||||
if strings.Contains(trimmed, ".") {
|
||||
trimmed = strings.ReplaceAll(trimmed, ",", "")
|
||||
} else {
|
||||
trimmed = strings.ReplaceAll(trimmed, ",", ".")
|
||||
}
|
||||
}
|
||||
|
||||
parsed, err := strconv.ParseFloat(trimmed, 64)
|
||||
if err != nil {
|
||||
return 0, false
|
||||
}
|
||||
return parsed, true
|
||||
}
|
||||
@@ -0,0 +1,738 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/uniformities/repositories"
|
||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/uniformities/validations"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type UniformityService interface {
|
||||
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlockKandangUniformity, int64, error)
|
||||
GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectFlockKandangUniformity, error)
|
||||
GetSummary(ctx *fiber.Ctx, id uint) (*entity.ProjectFlockKandangUniformity, error)
|
||||
CreateOne(ctx *fiber.Ctx, req *validation.Create, file *multipart.FileHeader, rows []BodyWeightExcelRow) (*entity.ProjectFlockKandangUniformity, error)
|
||||
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint, file *multipart.FileHeader, rows []BodyWeightExcelRow) (*entity.ProjectFlockKandangUniformity, error)
|
||||
DeleteOne(ctx *fiber.Ctx, id uint) error
|
||||
Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.ProjectFlockKandangUniformity, error)
|
||||
ParseBodyWeightExcel(ctx *fiber.Ctx, file *multipart.FileHeader) ([]BodyWeightExcelRow, error)
|
||||
ComputeUniformity(rows []BodyWeightExcelRow) (UniformityCalculation, error)
|
||||
CalculateUniformityFromDocument(ctx *fiber.Ctx, uniformityID uint) (UniformityCalculation, *entity.Document, error)
|
||||
}
|
||||
|
||||
type uniformityService struct {
|
||||
Log *logrus.Logger
|
||||
Validate *validator.Validate
|
||||
Repository repository.UniformityRepository
|
||||
DocumentSvc commonSvc.DocumentService
|
||||
ApprovalRepo commonRepo.ApprovalRepository
|
||||
ApprovalSvc commonSvc.ApprovalService
|
||||
}
|
||||
|
||||
func NewUniformityService(
|
||||
repo repository.UniformityRepository,
|
||||
documentSvc commonSvc.DocumentService,
|
||||
approvalRepo commonRepo.ApprovalRepository,
|
||||
approvalSvc commonSvc.ApprovalService,
|
||||
validate *validator.Validate,
|
||||
) UniformityService {
|
||||
return &uniformityService{
|
||||
Log: utils.Log,
|
||||
Validate: validate,
|
||||
Repository: repo,
|
||||
DocumentSvc: documentSvc,
|
||||
ApprovalRepo: approvalRepo,
|
||||
ApprovalSvc: approvalSvc,
|
||||
}
|
||||
}
|
||||
|
||||
func (s uniformityService) withRelations(db *gorm.DB) *gorm.DB {
|
||||
return db.
|
||||
Preload("ProjectFlockKandang.ProjectFlock.Location").
|
||||
Preload("ProjectFlockKandang.Kandang.Location")
|
||||
}
|
||||
|
||||
func (s uniformityService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlockKandangUniformity, int64, error) {
|
||||
if err := s.Validate.Struct(params); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
offset := (params.Page - 1) * params.Limit
|
||||
|
||||
uniformitys, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
||||
db = s.withRelations(db)
|
||||
if params.ProjectFlockKandangId != 0 {
|
||||
db = db.Where("project_flock_kandang_id = ?", params.ProjectFlockKandangId)
|
||||
}
|
||||
if params.Week != 0 {
|
||||
db = db.Where("week = ?", params.Week)
|
||||
}
|
||||
return db.Order("uniform_date DESC").Order("created_at DESC")
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to get uniformitys: %+v", err)
|
||||
return nil, 0, err
|
||||
}
|
||||
if err := s.attachLatestApprovals(c.Context(), uniformitys); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
return uniformitys, total, nil
|
||||
}
|
||||
|
||||
func (s uniformityService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectFlockKandangUniformity, error) {
|
||||
uniformity, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Uniformity not found")
|
||||
}
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed get uniformity by id: %+v", err)
|
||||
return nil, err
|
||||
}
|
||||
if err := s.attachLatestApproval(c.Context(), uniformity); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return uniformity, nil
|
||||
}
|
||||
|
||||
func (s uniformityService) GetSummary(c *fiber.Ctx, id uint) (*entity.ProjectFlockKandangUniformity, error) {
|
||||
return s.GetOne(c, id)
|
||||
}
|
||||
|
||||
func (s *uniformityService) CreateOne(c *fiber.Ctx, req *validation.Create, file *multipart.FileHeader, rows []BodyWeightExcelRow) (*entity.ProjectFlockKandangUniformity, error) {
|
||||
if err := s.Validate.Struct(req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if file == nil {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "document is required")
|
||||
}
|
||||
|
||||
uniformDate, err := time.Parse("2006-01-02", req.Date)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "date must be in YYYY-MM-DD format")
|
||||
}
|
||||
|
||||
if len(rows) == 0 {
|
||||
parsedRows, err := s.ParseBodyWeightExcel(c, file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rows = parsedRows
|
||||
}
|
||||
|
||||
calculation, err := s.ComputeUniformity(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
actorID, err := m.ActorIDFromContext(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
createBody := &entity.ProjectFlockKandangUniformity{
|
||||
Uniformity: calculation.Uniformity,
|
||||
Week: req.Week,
|
||||
Cv: calculation.Cv,
|
||||
ChickQtyOfWeight: calculation.ChickQtyOfWeight,
|
||||
MeanUp: calculation.MeanUp,
|
||||
MeanDown: calculation.MeanDown,
|
||||
ProjectFlockKandangId: req.ProjectFlockKandangId,
|
||||
UniformQty: calculation.UniformQty,
|
||||
NotUniformQty: calculation.OutsideQty,
|
||||
UniformDate: &uniformDate,
|
||||
CreatedBy: actorID,
|
||||
}
|
||||
|
||||
if err := s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
|
||||
repoTx := s.Repository.WithTx(tx)
|
||||
if err := repoTx.CreateOne(c.Context(), createBody, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.createUniformityApproval(
|
||||
c.Context(),
|
||||
tx,
|
||||
createBody.Id,
|
||||
utils.UniformityStepPengajuan,
|
||||
entity.ApprovalActionCreated,
|
||||
actorID,
|
||||
nil,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
s.Log.Errorf("Failed to create uniformity: %+v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if s.DocumentSvc != nil {
|
||||
actorIDCopy := actorID
|
||||
_, err := s.DocumentSvc.UploadDocuments(c.Context(), commonSvc.DocumentUploadRequest{
|
||||
DocumentableType: "UNIFORMITY",
|
||||
DocumentableID: uint64(createBody.Id),
|
||||
CreatedBy: &actorIDCopy,
|
||||
Files: []commonSvc.DocumentFile{
|
||||
{
|
||||
File: file,
|
||||
Type: "UNIFORMITY",
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
s.rollbackUniformityCreate(c.Context(), createBody.Id)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to upload uniformity document")
|
||||
}
|
||||
}
|
||||
|
||||
return s.GetOne(c, createBody.Id)
|
||||
}
|
||||
|
||||
func (s uniformityService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint, file *multipart.FileHeader, rows []BodyWeightExcelRow) (*entity.ProjectFlockKandangUniformity, error) {
|
||||
if err := s.Validate.Struct(req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
updateBody := make(map[string]any)
|
||||
|
||||
if req.Date != nil {
|
||||
parsed, err := time.Parse("2006-01-02", *req.Date)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "date must be in YYYY-MM-DD format")
|
||||
}
|
||||
updateBody["uniform_date"] = parsed
|
||||
}
|
||||
if req.ProjectFlockKandangId != nil {
|
||||
updateBody["project_flock_kandang_id"] = *req.ProjectFlockKandangId
|
||||
}
|
||||
if req.Week != nil {
|
||||
updateBody["week"] = *req.Week
|
||||
}
|
||||
|
||||
if file != nil {
|
||||
if s.DocumentSvc == nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Document service not available")
|
||||
}
|
||||
|
||||
if len(rows) == 0 {
|
||||
parsedRows, err := s.ParseBodyWeightExcel(c, file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rows = parsedRows
|
||||
}
|
||||
|
||||
calculation, err := s.ComputeUniformity(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
updateBody["uniformity"] = calculation.Uniformity
|
||||
updateBody["cv"] = calculation.Cv
|
||||
updateBody["chick_qty_of_weight"] = calculation.ChickQtyOfWeight
|
||||
updateBody["mean_up"] = calculation.MeanUp
|
||||
updateBody["mean_down"] = calculation.MeanDown
|
||||
updateBody["uniform_qty"] = calculation.UniformQty
|
||||
updateBody["not_uniform_qty"] = calculation.OutsideQty
|
||||
}
|
||||
|
||||
if len(updateBody) == 0 {
|
||||
return s.GetOne(c, id)
|
||||
}
|
||||
|
||||
if file == nil {
|
||||
if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Uniformity not found")
|
||||
}
|
||||
s.Log.Errorf("Failed to update uniformity: %+v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.GetOne(c, id)
|
||||
}
|
||||
|
||||
existingDocs, err := s.DocumentSvc.ListByTarget(c.Context(), "UNIFORMITY", uint64(id))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
actorID, err := m.ActorIDFromContext(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
actorIDCopy := actorID
|
||||
uploadResults, err := s.DocumentSvc.UploadDocuments(c.Context(), commonSvc.DocumentUploadRequest{
|
||||
DocumentableType: "UNIFORMITY",
|
||||
DocumentableID: uint64(id),
|
||||
CreatedBy: &actorIDCopy,
|
||||
Files: []commonSvc.DocumentFile{
|
||||
{
|
||||
File: file,
|
||||
Type: "UNIFORMITY",
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to upload uniformity document")
|
||||
}
|
||||
|
||||
if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil {
|
||||
if len(uploadResults) > 0 {
|
||||
ids := make([]uint, 0, len(uploadResults))
|
||||
for _, result := range uploadResults {
|
||||
if result.Document.Id != 0 {
|
||||
ids = append(ids, result.Document.Id)
|
||||
}
|
||||
}
|
||||
if len(ids) > 0 {
|
||||
_ = s.DocumentSvc.DeleteDocuments(c.Context(), ids, true)
|
||||
}
|
||||
}
|
||||
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Uniformity not found")
|
||||
}
|
||||
s.Log.Errorf("Failed to update uniformity: %+v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(existingDocs) > 0 {
|
||||
oldIDs := make([]uint, 0, len(existingDocs))
|
||||
for _, doc := range existingDocs {
|
||||
if doc.Id != 0 {
|
||||
oldIDs = append(oldIDs, doc.Id)
|
||||
}
|
||||
}
|
||||
if len(oldIDs) > 0 {
|
||||
_ = s.DocumentSvc.DeleteDocuments(c.Context(), oldIDs, true)
|
||||
}
|
||||
}
|
||||
|
||||
return s.GetOne(c, id)
|
||||
}
|
||||
|
||||
func (s uniformityService) DeleteOne(c *fiber.Ctx, id uint) error {
|
||||
if err := s.Repository.DeleteOne(c.Context(), id); err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return fiber.NewError(fiber.StatusNotFound, "Uniformity not found")
|
||||
}
|
||||
s.Log.Errorf("Failed to delete uniformity: %+v", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s uniformityService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entity.ProjectFlockKandangUniformity, error) {
|
||||
if err := s.Validate.Struct(req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
actionValue := strings.ToUpper(strings.TrimSpace(req.Action))
|
||||
var action entity.ApprovalAction
|
||||
switch actionValue {
|
||||
case string(entity.ApprovalActionApproved):
|
||||
action = entity.ApprovalActionApproved
|
||||
case string(entity.ApprovalActionRejected):
|
||||
action = entity.ApprovalActionRejected
|
||||
default:
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "action must be APPROVED or REJECTED")
|
||||
}
|
||||
|
||||
ids := uniqueUintSlice(req.ApprovableIds)
|
||||
if len(ids) == 0 {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "approvable_ids must contain at least one id")
|
||||
}
|
||||
|
||||
step := utils.UniformityStepPengajuan
|
||||
if action == entity.ApprovalActionApproved {
|
||||
step = utils.UniformityStepDisetujui
|
||||
}
|
||||
|
||||
actorID, err := m.ActorIDFromContext(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ctx := c.Context()
|
||||
transactionErr := s.Repository.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
repoTx := s.Repository.WithTx(tx)
|
||||
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(tx))
|
||||
|
||||
for _, id := range ids {
|
||||
if _, err := repoTx.GetByID(ctx, id, nil); err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Uniformity %d not found", id))
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := approvalSvc.CreateApproval(
|
||||
ctx,
|
||||
utils.ApprovalWorkflowUniformity,
|
||||
id,
|
||||
step,
|
||||
&action,
|
||||
actorID,
|
||||
req.Notes,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if transactionErr != nil {
|
||||
if fiberErr, ok := transactionErr.(*fiber.Error); ok {
|
||||
return nil, fiberErr
|
||||
}
|
||||
s.Log.Errorf("Failed to record approvals for uniformities %+v: %+v", ids, transactionErr)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to submit uniformity approval")
|
||||
}
|
||||
|
||||
results := make([]entity.ProjectFlockKandangUniformity, 0, len(ids))
|
||||
for _, id := range ids {
|
||||
loaded, err := s.GetOne(c, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
results = append(results, *loaded)
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
type UniformityDetailItem struct {
|
||||
Id int
|
||||
Weight float64
|
||||
Range string
|
||||
}
|
||||
|
||||
type UniformityCalculation struct {
|
||||
ChickQtyOfWeight float64
|
||||
MeanWeight float64
|
||||
MeanDown float64
|
||||
MeanUp float64
|
||||
UniformQty float64
|
||||
OutsideQty float64
|
||||
Uniformity float64
|
||||
Cv float64
|
||||
Details []UniformityDetailItem
|
||||
}
|
||||
|
||||
func (s uniformityService) ComputeUniformity(rows []BodyWeightExcelRow) (UniformityCalculation, error) {
|
||||
return computeUniformity(rows)
|
||||
}
|
||||
|
||||
func (s uniformityService) CalculateUniformityFromDocument(c *fiber.Ctx, uniformityID uint) (UniformityCalculation, *entity.Document, error) {
|
||||
if s.DocumentSvc == nil {
|
||||
return UniformityCalculation{}, nil, fiber.NewError(fiber.StatusInternalServerError, "Document service not available")
|
||||
}
|
||||
|
||||
documents, err := s.DocumentSvc.ListByTarget(c.Context(), "UNIFORMITY", uint64(uniformityID))
|
||||
if err != nil {
|
||||
return UniformityCalculation{}, nil, err
|
||||
}
|
||||
if len(documents) == 0 {
|
||||
return UniformityCalculation{}, nil, fiber.NewError(fiber.StatusNotFound, "Uniformity document not found")
|
||||
}
|
||||
|
||||
document := documents[0]
|
||||
url := s.DocumentSvc.PublicURL(document)
|
||||
if url == "" {
|
||||
return UniformityCalculation{}, nil, fiber.NewError(fiber.StatusBadRequest, "Uniformity document URL not available")
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(c.Context(), http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return UniformityCalculation{}, nil, err
|
||||
}
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return UniformityCalculation{}, nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return UniformityCalculation{}, nil, fiber.NewError(fiber.StatusBadRequest, "Failed to download uniformity document")
|
||||
}
|
||||
|
||||
rows, err := parseBodyWeightExcelReader(resp.Body)
|
||||
if err != nil {
|
||||
return UniformityCalculation{}, nil, err
|
||||
}
|
||||
|
||||
calculation, err := computeUniformity(rows)
|
||||
if err != nil {
|
||||
return UniformityCalculation{}, nil, err
|
||||
}
|
||||
|
||||
return calculation, &document, nil
|
||||
}
|
||||
|
||||
func (s *uniformityService) createUniformityApproval(
|
||||
ctx context.Context,
|
||||
db *gorm.DB,
|
||||
uniformityID uint,
|
||||
step approvalutils.ApprovalStep,
|
||||
action entity.ApprovalAction,
|
||||
actorID uint,
|
||||
notes *string,
|
||||
) error {
|
||||
if uniformityID == 0 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Uniformity tidak valid untuk approval")
|
||||
}
|
||||
if actorID == 0 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Actor Id tidak valid untuk approval")
|
||||
}
|
||||
|
||||
var svc commonSvc.ApprovalService
|
||||
if db != nil {
|
||||
svc = commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(db))
|
||||
} else if s.ApprovalSvc != nil {
|
||||
svc = s.ApprovalSvc
|
||||
} else {
|
||||
svc = commonSvc.NewApprovalService(s.ApprovalRepo)
|
||||
}
|
||||
|
||||
_, err := svc.CreateApproval(ctx, utils.ApprovalWorkflowUniformity, uniformityID, step, &action, actorID, notes)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *uniformityService) attachLatestApprovals(ctx context.Context, items []entity.ProjectFlockKandangUniformity) error {
|
||||
if len(items) == 0 || s.ApprovalSvc == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
ids := make([]uint, 0, len(items))
|
||||
visited := make(map[uint]struct{}, len(items))
|
||||
for _, item := range items {
|
||||
if item.Id == 0 {
|
||||
continue
|
||||
}
|
||||
if _, ok := visited[item.Id]; ok {
|
||||
continue
|
||||
}
|
||||
visited[item.Id] = struct{}{}
|
||||
ids = append(ids, item.Id)
|
||||
}
|
||||
|
||||
if len(ids) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
latestMap, err := s.ApprovalSvc.LatestByTargets(ctx, utils.ApprovalWorkflowUniformity, ids, func(db *gorm.DB) *gorm.DB {
|
||||
return db.Preload("ActionUser")
|
||||
})
|
||||
if err != nil {
|
||||
s.Log.Warnf("Unable to load latest approvals for uniformities: %+v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(latestMap) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
for i := range items {
|
||||
if items[i].Id == 0 {
|
||||
continue
|
||||
}
|
||||
if approval, ok := latestMap[items[i].Id]; ok {
|
||||
items[i].LatestApproval = approval
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *uniformityService) attachLatestApproval(ctx context.Context, item *entity.ProjectFlockKandangUniformity) error {
|
||||
if item == nil || item.Id == 0 || s.ApprovalSvc == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
approvals, err := s.ApprovalSvc.ListByTarget(ctx, utils.ApprovalWorkflowUniformity, item.Id, func(db *gorm.DB) *gorm.DB {
|
||||
return db.Preload("ActionUser")
|
||||
})
|
||||
if err != nil {
|
||||
s.Log.Warnf("Unable to load approvals for uniformity %d: %+v", item.Id, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(approvals) == 0 {
|
||||
item.LatestApproval = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
latest := approvals[len(approvals)-1]
|
||||
item.LatestApproval = &latest
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *uniformityService) rollbackUniformityCreate(ctx context.Context, uniformityID uint) {
|
||||
if uniformityID == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if s.ApprovalRepo != nil {
|
||||
if err := s.ApprovalRepo.DeleteByTarget(ctx, utils.ApprovalWorkflowUniformity.String(), uniformityID); err != nil {
|
||||
s.Log.WithError(err).Warnf("Failed to rollback uniformity approvals for %d", uniformityID)
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.Repository.DeleteOne(ctx, uniformityID); err != nil {
|
||||
s.Log.WithError(err).Warnf("Failed to rollback uniformity %d", uniformityID)
|
||||
}
|
||||
}
|
||||
|
||||
func uniqueUintSlice(values []uint) []uint {
|
||||
if len(values) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
seen := make(map[uint]struct{}, len(values))
|
||||
result := make([]uint, 0, len(values))
|
||||
for _, v := range values {
|
||||
if v == 0 {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[v]; ok {
|
||||
continue
|
||||
}
|
||||
seen[v] = struct{}{}
|
||||
result = append(result, v)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func computeUniformity(rows []BodyWeightExcelRow) (UniformityCalculation, error) {
|
||||
weights := make([]float64, 0, len(rows))
|
||||
details := make([]UniformityDetailItem, 0, len(rows))
|
||||
hasRangeLabels := false
|
||||
for idx, row := range rows {
|
||||
if row.Weight <= 0 {
|
||||
continue
|
||||
}
|
||||
id := row.No
|
||||
if id <= 0 {
|
||||
id = idx + 1
|
||||
}
|
||||
weights = append(weights, row.Weight)
|
||||
rangeLabel := strings.TrimSpace(row.Range)
|
||||
if rangeLabel != "" {
|
||||
upper := strings.ToUpper(rangeLabel)
|
||||
if upper == "HIGH" || upper == "LOW" {
|
||||
hasRangeLabels = true
|
||||
}
|
||||
rangeLabel = upper
|
||||
}
|
||||
details = append(details, UniformityDetailItem{
|
||||
Id: id,
|
||||
Weight: row.Weight,
|
||||
Range: rangeLabel,
|
||||
})
|
||||
}
|
||||
|
||||
total := float64(len(weights))
|
||||
if total == 0 {
|
||||
return UniformityCalculation{}, fiber.NewError(fiber.StatusBadRequest, "no body weight data found")
|
||||
}
|
||||
|
||||
var sum float64
|
||||
for _, w := range weights {
|
||||
sum += w
|
||||
}
|
||||
mean := sum / total
|
||||
meanUpThreshold := roundToPrecision(mean*1.10, 3)
|
||||
meanDownThreshold := roundToPrecision(mean*0.90, 3)
|
||||
|
||||
var uniformCount float64
|
||||
for i := range details {
|
||||
if hasRangeLabels {
|
||||
if details[i].Range == "HIGH" || details[i].Range == "LOW" {
|
||||
details[i].Range = "Outside"
|
||||
continue
|
||||
}
|
||||
details[i].Range = "Ideal"
|
||||
uniformCount++
|
||||
continue
|
||||
}
|
||||
|
||||
w := details[i].Weight
|
||||
if w > meanUpThreshold || w < meanDownThreshold {
|
||||
details[i].Range = "Outside"
|
||||
continue
|
||||
}
|
||||
details[i].Range = "Ideal"
|
||||
uniformCount++
|
||||
}
|
||||
outsideCount := total - uniformCount
|
||||
|
||||
var cv float64
|
||||
if mean > 0 && total > 1 {
|
||||
stddevWeights := weights
|
||||
if len(stddevWeights) > 100 {
|
||||
stddevWeights = stddevWeights[:100]
|
||||
}
|
||||
stddevCount := float64(len(stddevWeights))
|
||||
if stddevCount > 1 {
|
||||
var stddevSum float64
|
||||
for _, w := range stddevWeights {
|
||||
stddevSum += w
|
||||
}
|
||||
stddevMean := stddevSum / stddevCount
|
||||
var sumSquares float64
|
||||
for _, w := range stddevWeights {
|
||||
diff := w - stddevMean
|
||||
sumSquares += diff * diff
|
||||
}
|
||||
stddev := math.Sqrt(sumSquares / (stddevCount - 1))
|
||||
cv = (stddev / mean) * 100
|
||||
}
|
||||
}
|
||||
|
||||
uniformity := (uniformCount / total) * 100
|
||||
|
||||
return UniformityCalculation{
|
||||
ChickQtyOfWeight: total,
|
||||
MeanWeight: roundToPrecision(mean, 0),
|
||||
MeanDown: roundToPrecision(mean*0.90, 0),
|
||||
MeanUp: roundToPrecision(mean*1.10, 0),
|
||||
UniformQty: uniformCount,
|
||||
OutsideQty: outsideCount,
|
||||
Uniformity: roundToPrecision(uniformity, 0),
|
||||
Cv: roundToPrecision(cv, 1),
|
||||
Details: details,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func roundToPrecision(value float64, precision int) float64 {
|
||||
if precision < 0 {
|
||||
return value
|
||||
}
|
||||
scale := math.Pow10(precision)
|
||||
scaled := value * scale
|
||||
fraction := scaled - math.Floor(scaled)
|
||||
if fraction >= 0.5 {
|
||||
return math.Ceil(scaled) / scale
|
||||
}
|
||||
return math.Floor(scaled) / scale
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
package validation
|
||||
|
||||
import (
|
||||
"mime/multipart"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
type Create struct {
|
||||
Date string `form:"date" validate:"required"`
|
||||
ProjectFlockKandangId uint `form:"project_flock_kandang_id" validate:"required,number,min=1"`
|
||||
Week int `form:"week" validate:"required,min=1"`
|
||||
}
|
||||
|
||||
type Update struct {
|
||||
Date *string `json:"date,omitempty" form:"date" validate:"omitempty"`
|
||||
ProjectFlockKandangId *uint `json:"project_flock_kandang_id,omitempty" form:"project_flock_kandang_id" validate:"omitempty,number,min=1"`
|
||||
Week *int `json:"week,omitempty" form:"week" validate:"omitempty,min=1"`
|
||||
}
|
||||
|
||||
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"`
|
||||
ProjectFlockKandangId uint `query:"project_flock_kandang_id" validate:"omitempty,number,min=1"`
|
||||
Week int `query:"week" validate:"omitempty,min=1"`
|
||||
}
|
||||
|
||||
type UploadExcelRequest struct {
|
||||
Documents []*multipart.FileHeader `form:"documents" json:"documents" validate:"omitempty,dive"`
|
||||
}
|
||||
|
||||
type Approve struct {
|
||||
Action string `json:"action" validate:"required_strict"`
|
||||
ApprovableIds []uint `json:"approvable_ids" validate:"required_strict,min=1,dive,gt=0"`
|
||||
Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"`
|
||||
}
|
||||
|
||||
func ParseIDParam(c *fiber.Ctx, name string) (uint, error) {
|
||||
raw := strings.TrimSpace(c.Params(name))
|
||||
if raw == "" {
|
||||
return 0, fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||
}
|
||||
id, err := strconv.Atoi(raw)
|
||||
if err != nil || id <= 0 {
|
||||
return 0, fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||
}
|
||||
return uint(id), nil
|
||||
}
|
||||
|
||||
func ParseQuery(c *fiber.Ctx) (*Query, error) {
|
||||
query := &Query{
|
||||
Page: c.QueryInt("page", 1),
|
||||
Limit: c.QueryInt("limit", 10),
|
||||
ProjectFlockKandangId: uint(c.QueryInt("project_flock_kandang_id", 0)),
|
||||
Week: c.QueryInt("week", 0),
|
||||
}
|
||||
|
||||
if query.Page < 1 || query.Limit < 1 {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
|
||||
}
|
||||
|
||||
return query, nil
|
||||
}
|
||||
|
||||
func ParseCreate(c *fiber.Ctx) (*Create, *multipart.FileHeader, error) {
|
||||
date := strings.TrimSpace(c.FormValue("date"))
|
||||
if date == "" {
|
||||
return nil, nil, fiber.NewError(fiber.StatusBadRequest, "date is required")
|
||||
}
|
||||
|
||||
projectFlockKandangIDStr := strings.TrimSpace(c.FormValue("project_flock_kandang_id"))
|
||||
if projectFlockKandangIDStr == "" {
|
||||
return nil, nil, fiber.NewError(fiber.StatusBadRequest, "project_flock_kandang_id is required")
|
||||
}
|
||||
|
||||
projectFlockKandangID, err := strconv.Atoi(projectFlockKandangIDStr)
|
||||
if err != nil || projectFlockKandangID <= 0 {
|
||||
return nil, nil, fiber.NewError(fiber.StatusBadRequest, "project_flock_kandang_id is required")
|
||||
}
|
||||
|
||||
weekStr := strings.TrimSpace(c.FormValue("week"))
|
||||
if weekStr == "" {
|
||||
return nil, nil, fiber.NewError(fiber.StatusBadRequest, "week is required")
|
||||
}
|
||||
|
||||
week, err := strconv.Atoi(weekStr)
|
||||
if err != nil || week <= 0 {
|
||||
return nil, nil, fiber.NewError(fiber.StatusBadRequest, "week is required")
|
||||
}
|
||||
|
||||
file, err := c.FormFile("document")
|
||||
if err != nil {
|
||||
return nil, nil, fiber.NewError(fiber.StatusBadRequest, "document is required")
|
||||
}
|
||||
|
||||
return &Create{
|
||||
Date: date,
|
||||
ProjectFlockKandangId: uint(projectFlockKandangID),
|
||||
Week: week,
|
||||
}, file, nil
|
||||
}
|
||||
|
||||
func ParseUpdate(c *fiber.Ctx) (*Update, *multipart.FileHeader, error) {
|
||||
contentType := strings.ToLower(c.Get("Content-Type"))
|
||||
if strings.Contains(contentType, "multipart/form-data") {
|
||||
req := &Update{}
|
||||
|
||||
date := strings.TrimSpace(c.FormValue("date"))
|
||||
if date != "" {
|
||||
req.Date = &date
|
||||
}
|
||||
|
||||
projectFlockKandangIDStr := strings.TrimSpace(c.FormValue("project_flock_kandang_id"))
|
||||
if projectFlockKandangIDStr != "" {
|
||||
projectFlockKandangID, err := strconv.Atoi(projectFlockKandangIDStr)
|
||||
if err != nil || projectFlockKandangID <= 0 {
|
||||
return nil, nil, fiber.NewError(fiber.StatusBadRequest, "project_flock_kandang_id is invalid")
|
||||
}
|
||||
idCopy := uint(projectFlockKandangID)
|
||||
req.ProjectFlockKandangId = &idCopy
|
||||
}
|
||||
|
||||
weekStr := strings.TrimSpace(c.FormValue("week"))
|
||||
if weekStr != "" {
|
||||
week, err := strconv.Atoi(weekStr)
|
||||
if err != nil || week <= 0 {
|
||||
return nil, nil, fiber.NewError(fiber.StatusBadRequest, "week is invalid")
|
||||
}
|
||||
req.Week = &week
|
||||
}
|
||||
|
||||
file, err := c.FormFile("document")
|
||||
if err != nil {
|
||||
file = nil
|
||||
}
|
||||
|
||||
return req, file, nil
|
||||
}
|
||||
|
||||
req := new(Update)
|
||||
if err := c.BodyParser(req); err != nil {
|
||||
return nil, nil, fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||
}
|
||||
return req, nil, nil
|
||||
}
|
||||
|
||||
func ParseUploadFiles(c *fiber.Ctx) ([]*multipart.FileHeader, error) {
|
||||
form, err := c.MultipartForm()
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid multipart form")
|
||||
}
|
||||
|
||||
files := form.File["documents"]
|
||||
if len(files) == 0 {
|
||||
if file, err := c.FormFile("document"); err == nil && file != nil {
|
||||
files = []*multipart.FileHeader{file}
|
||||
} else {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "documents is required")
|
||||
}
|
||||
}
|
||||
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func ParseApprove(c *fiber.Ctx) (*Approve, error) {
|
||||
req := new(Approve)
|
||||
if err := c.BodyParser(req); err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
Reference in New Issue
Block a user