diff --git a/.DS_Store b/.DS_Store index e39247fd..be6f22d7 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/internal/modules/production/recordings/dto/recording.dto.go b/internal/modules/production/recordings/dto/recording.dto.go index f5a04821..4deb9f7e 100644 --- a/internal/modules/production/recordings/dto/recording.dto.go +++ b/internal/modules/production/recordings/dto/recording.dto.go @@ -14,44 +14,83 @@ import ( // === DTO Structs === +type RecordingProjectFlockDTO struct { + ProjectFlockKandangId uint `json:"project_flock_kandang_id"` + FlockName string `json:"flock_name"` + ProjectFlockCategory string `json:"project_flock_category"` + Period int `json:"period"` + ProductionStandart *RecordingProductionStandardDTO `json:"production_standart,omitempty"` + Fcr *RecordingFcrDTO `json:"fcr,omitempty"` + TotalChickQty float64 `json:"total_chick_qty"` +} + +type RecordingProductionStandardDTO struct { + Id uint `json:"id"` + Week int `json:"week"` + Name string `json:"name"` + HenDayStd float64 `json:"hen_day_std"` + HenHouseStd float64 `json:"hen_house_std"` + FeedIntakeStd float64 `json:"feed_intake_std"` + MaxDepletionStd float64 `json:"max_depletion_std"` + EggMassStd float64 `json:"egg_mass_std"` + EggWeightStd float64 `json:"egg_weight_std"` +} + +type RecordingFcrDTO struct { + Id uint `json:"id"` + Name string `json:"name"` + FcrStd float64 `json:"fcr_std"` +} + +type RecordingAreaDTO struct { + Id uint `json:"id"` + Name string `json:"name"` +} + +type RecordingLocationDTO struct { + Id uint `json:"id"` + Name string `json:"name"` + Address string `json:"address"` +} + +type RecordingWarehouseDTO struct { + Id uint `json:"id"` + Name string `json:"name"` + Area *RecordingAreaDTO `json:"area,omitempty"` + Location *RecordingLocationDTO `json:"location,omitempty"` +} + type RecordingRelationDTO struct { - Id uint `json:"id"` - ProjectFlockKandangId uint `json:"project_flock_kandang_id"` - RecordDatetime time.Time `json:"record_datetime"` - Day int `json:"day"` - ProjectFlockCategory string `json:"project_flock_category"` - TotalDepletionQty float64 `json:"total_depletion_qty"` - CumDepletionRate float64 `json:"cum_depletion_rate"` - CumIntake int `json:"cum_intake"` - FcrValue float64 `json:"fcr_value"` - TotalChickQty float64 `json:"total_chick_qty"` - HenDay float64 `json:"hen_day"` - HenHouse float64 `json:"hen_house"` - FeedIntake float64 `json:"feed_intake"` - EggMass float64 `json:"egg_mass"` - EggWeight float64 `json:"egg_weight"` - StandardHenDay *float64 `json:"hen_day_std,omitempty"` - StandardHenHouse *float64 `json:"hen_house_std,omitempty"` - StandardFeedIntake *float64 `json:"feed_intake_std,omitempty"` - StandardMaxDepletion *float64 `json:"max_depletion_std,omitempty"` - StandardEggMass *float64 `json:"egg_mass_std,omitempty"` - StandardEggWeight *float64 `json:"egg_weight_std,omitempty"` - StandardFcr *float64 `json:"fcr_std,omitempty"` - Approval approvalDTO.ApprovalRelationDTO `json:"approval"` + Id uint `json:"id"` + ProjectFlock RecordingProjectFlockDTO `json:"project_flock"` + RecordDatetime time.Time `json:"record_datetime"` + Day int `json:"day"` + TotalDepletionQty float64 `json:"total_depletion_qty"` + CumDepletionRate float64 `json:"cum_depletion_rate"` + CumIntake int `json:"cum_intake"` + FcrValue float64 `json:"fcr_value"` + HenDay float64 `json:"hen_day"` + HenHouse float64 `json:"hen_house"` + FeedIntake float64 `json:"feed_intake"` + EggMass float64 `json:"egg_mass"` + EggWeight float64 `json:"egg_weight"` + Approval approvalDTO.ApprovalRelationDTO `json:"approval"` } type RecordingListDTO struct { RecordingRelationDTO - CreatedUser *userDTO.UserRelationDTO `json:"created_user"` + CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } type RecordingDetailDTO struct { RecordingListDTO - Depletions []RecordingDepletionDTO `json:"depletions"` - Stocks []RecordingStockDTO `json:"stocks"` - Eggs []RecordingEggDTO `json:"eggs"` + Warehouse *RecordingWarehouseDTO `json:"warehouse,omitempty"` + ProductCategory string `json:"product_category"` + Depletions []RecordingDepletionDTO `json:"depletions"` + Stocks []RecordingStockDTO `json:"stocks"` + Eggs []RecordingEggDTO `json:"eggs"` } type RecordingDepletionDTO struct { @@ -63,7 +102,7 @@ type RecordingDepletionDTO struct { type RecordingStockDTO struct { ProductWarehouseId uint `json:"product_warehouse_id"` UsageAmount float64 `json:"usage_amount"` - PendingQty *float64 `json:"pending_qty,omitempty"` + PendingQty float64 `json:"pending_qty"` ProductWarehouse productWarehouseDTO.ProductWarehouseDTO `json:"product_warehouse"` } @@ -75,117 +114,10 @@ type RecordingEggDTO struct { ProductWarehouse productWarehouseDTO.ProductWarehouseDTO `json:"product_warehouse"` } -type RecordingProductWarehouseDTO struct { - Id uint `json:"id"` - ProductId uint `json:"product_id"` - ProductName string `json:"product_name"` - WarehouseId uint `json:"warehouse_id"` - WarehouseName string `json:"warehouse_name"` -} - // === Mapper Functions === -func ToRecordingRelationDTO(e entity.Recording) RecordingRelationDTO { - var ( - projectFlockCategory string - day int - totalDepletionQty float64 - cumDepletionRate float64 - cumIntake int - fcrValue float64 - totalChickQty float64 - henDay float64 - henHouse float64 - feedIntake float64 - eggMass float64 - eggWeight float64 - ) - - if e.Day != nil { - day = *e.Day - } - if e.TotalDepletionQty != nil { - totalDepletionQty = *e.TotalDepletionQty - } - if e.CumDepletionRate != nil { - cumDepletionRate = *e.CumDepletionRate - } - if e.CumIntake != nil { - cumIntake = *e.CumIntake - } - if e.FcrValue != nil { - fcrValue = *e.FcrValue - } - if e.TotalChickQty != nil { - totalChickQty = *e.TotalChickQty - } - if e.HenDay != nil { - henDay = *e.HenDay - } - if e.HenHouse != nil { - henHouse = *e.HenHouse - } - if e.FeedIntake != nil { - feedIntake = *e.FeedIntake - } - if e.EggMass != nil { - eggMass = *e.EggMass - } - if e.EggWeight != nil { - eggWeight = *e.EggWeight - } - - if e.ProjectFlockKandang != nil && e.ProjectFlockKandang.ProjectFlock.Id != 0 { - category := e.ProjectFlockKandang.ProjectFlock.Category - projectFlockCategory = category - } - - latestApproval := defaultRecordingLatestApproval(e) - if e.LatestApproval != nil { - snapshot := approvalDTO.ToApprovalDTO(*e.LatestApproval) - latestApproval = snapshot - } - - return RecordingRelationDTO{ - Id: e.Id, - ProjectFlockKandangId: e.ProjectFlockKandangId, - RecordDatetime: e.RecordDatetime, - Day: day, - ProjectFlockCategory: projectFlockCategory, - TotalDepletionQty: totalDepletionQty, - CumDepletionRate: cumDepletionRate, - CumIntake: cumIntake, - FcrValue: fcrValue, - TotalChickQty: totalChickQty, - HenDay: henDay, - HenHouse: henHouse, - FeedIntake: feedIntake, - EggMass: eggMass, - EggWeight: eggWeight, - StandardHenDay: e.StandardHenDay, - StandardHenHouse: e.StandardHenHouse, - StandardFeedIntake: e.StandardFeedIntake, - StandardMaxDepletion: e.StandardMaxDepletion, - StandardEggMass: e.StandardEggMass, - StandardEggWeight: e.StandardEggWeight, - StandardFcr: e.StandardFcr, - Approval: latestApproval, - } -} - func ToRecordingListDTO(e entity.Recording) RecordingListDTO { - var createdUser *userDTO.UserRelationDTO - if e.CreatedUser != nil && e.CreatedUser.Id != 0 { - mapped := userDTO.ToUserRelationDTO(*e.CreatedUser) - createdUser = &mapped - } - - return RecordingListDTO{ - RecordingRelationDTO: ToRecordingRelationDTO(e), - CreatedAt: e.CreatedAt, - UpdatedAt: e.UpdatedAt, - CreatedUser: createdUser, - } + return toRecordingListDTO(e) } func ToRecordingListDTOs(e []entity.Recording) []RecordingListDTO { @@ -197,20 +129,15 @@ func ToRecordingListDTOs(e []entity.Recording) []RecordingListDTO { } func ToRecordingDetailDTO(e entity.Recording) RecordingDetailDTO { - listDTO := ToRecordingListDTO(e) - - var eggs []RecordingEggDTO - if strings.EqualFold(listDTO.ProjectFlockCategory, string(utils.ProjectFlockCategoryLaying)) { - eggs = ToRecordingEggDTOs(e.Eggs) - } else if len(e.Eggs) > 0 { - eggs = ToRecordingEggDTOs(e.Eggs) - } + listDTO := toRecordingListDTO(e) return RecordingDetailDTO{ RecordingListDTO: listDTO, - Depletions: ToRecordingDepletionDTOs(e.Depletions), - Stocks: ToRecordingStockDTOs(e.Stocks), - Eggs: eggs, + Warehouse: recordingWarehouseDTO(e), + ProductCategory: recordingProductCategory(e), + Depletions: ToRecordingDepletionDTOs(e.Depletions), + Stocks: ToRecordingStockDTOs(e.Stocks), + Eggs: ToRecordingEggDTOs(e.Eggs), } } @@ -233,11 +160,15 @@ func ToRecordingStockDTOs(stocks []entity.RecordingStock) []RecordingStockDTO { if s.UsageQty != nil { usageAmount = *s.UsageQty } + var pendingQty float64 + if s.PendingQty != nil { + pendingQty = *s.PendingQty + } result[i] = RecordingStockDTO{ ProductWarehouseId: s.ProductWarehouseId, UsageAmount: usageAmount, - PendingQty: s.PendingQty, + PendingQty: pendingQty, ProductWarehouse: mapProductWarehouseDTO(&s.ProductWarehouse), } } @@ -258,6 +189,184 @@ func ToRecordingEggDTOs(eggs []entity.RecordingEgg) []RecordingEggDTO { return result } +func toRecordingListDTO(e entity.Recording) RecordingListDTO { + relation := toRecordingRelationDTO(e) + + var createdUser *userDTO.UserRelationDTO + if e.CreatedUser != nil && e.CreatedUser.Id != 0 { + mapped := userDTO.ToUserRelationDTO(*e.CreatedUser) + createdUser = &mapped + } + + return RecordingListDTO{ + RecordingRelationDTO: relation, + CreatedAt: e.CreatedAt, + UpdatedAt: e.UpdatedAt, + CreatedUser: createdUser, + } +} + +func toRecordingRelationDTO(e entity.Recording) RecordingRelationDTO { + latestApproval := defaultRecordingLatestApproval(e) + if e.LatestApproval != nil { + snapshot := approvalDTO.ToApprovalDTO(*e.LatestApproval) + latestApproval = snapshot + } + + return RecordingRelationDTO{ + Id: e.Id, + ProjectFlock: toRecordingProjectFlockDTO(e), + RecordDatetime: e.RecordDatetime, + Day: intValue(e.Day), + TotalDepletionQty: floatValue(e.TotalDepletionQty), + CumDepletionRate: floatValue(e.CumDepletionRate), + CumIntake: intValue(e.CumIntake), + FcrValue: floatValue(e.FcrValue), + HenDay: floatValue(e.HenDay), + HenHouse: floatValue(e.HenHouse), + FeedIntake: floatValue(e.FeedIntake), + EggMass: floatValue(e.EggMass), + EggWeight: floatValue(e.EggWeight), + Approval: latestApproval, + } +} + +func toRecordingProjectFlockDTO(e entity.Recording) RecordingProjectFlockDTO { + result := RecordingProjectFlockDTO{ + ProjectFlockKandangId: e.ProjectFlockKandangId, + } + + pfk := e.ProjectFlockKandang + if pfk == nil { + return result + } + + if pfk.ProjectFlock.Id != 0 { + result.FlockName = pfk.ProjectFlock.FlockName + if pfk.ProjectFlock.Category != "" { + result.ProjectFlockCategory = strings.ToUpper(pfk.ProjectFlock.Category) + } + } + + result.Period = pfk.Period + + if pfk.ProjectFlock.ProductionStandard.Id != 0 { + result.ProductionStandart = &RecordingProductionStandardDTO{ + Id: pfk.ProjectFlock.ProductionStandard.Id, + Week: recordingWeekValue(e), + Name: pfk.ProjectFlock.ProductionStandard.Name, + HenDayStd: floatValue(e.StandardHenDay), + HenHouseStd: floatValue(e.StandardHenHouse), + FeedIntakeStd: floatValue(e.StandardFeedIntake), + MaxDepletionStd: floatValue(e.StandardMaxDepletion), + EggMassStd: floatValue(e.StandardEggMass), + EggWeightStd: floatValue(e.StandardEggWeight), + } + } + + if pfk.ProjectFlock.Fcr.Id != 0 || e.StandardFcr != nil { + result.Fcr = &RecordingFcrDTO{ + Id: pfk.ProjectFlock.Fcr.Id, + Name: pfk.ProjectFlock.Fcr.Name, + FcrStd: floatValue(e.StandardFcr), + } + } + + result.TotalChickQty = floatValue(e.TotalChickQty) + + return result +} + +func recordingWeekValue(e entity.Recording) int { + day := intValue(e.Day) + if day <= 0 { + return 0 + } + weekBase := 1 + if isLayingRecording(e) { + weekBase = 18 + } + return ((day - 1) / 7) + weekBase +} + +func isLayingRecording(e entity.Recording) bool { + if e.ProjectFlockKandang == nil { + return false + } + return strings.EqualFold(e.ProjectFlockKandang.ProjectFlock.Category, string(utils.ProjectFlockCategoryLaying)) +} + +func recordingProductCategory(e entity.Recording) string { + if e.ProjectFlockKandang == nil { + return "" + } + project := e.ProjectFlockKandang.ProjectFlock + if project.Id == 0 { + return "" + } + if project.ProductionStandard.Id != 0 && project.ProductionStandard.ProjectCategory != "" { + return strings.ToUpper(project.ProductionStandard.ProjectCategory) + } + if project.Category != "" { + return strings.ToUpper(project.Category) + } + return "" +} + +func recordingWarehouseDTO(e entity.Recording) *RecordingWarehouseDTO { + pw := primaryProductWarehouse(e) + if pw == nil || pw.Warehouse.Id == 0 { + return nil + } + return mapWarehouseDTO(&pw.Warehouse) +} + +func primaryProductWarehouse(e entity.Recording) *entity.ProductWarehouse { + if len(e.Stocks) > 0 { + pw := e.Stocks[0].ProductWarehouse + if pw.Id != 0 { + return &pw + } + } + if len(e.Depletions) > 0 { + pw := e.Depletions[0].ProductWarehouse + if pw.Id != 0 { + return &pw + } + } + if len(e.Eggs) > 0 { + pw := e.Eggs[0].ProductWarehouse + if pw.Id != 0 { + return &pw + } + } + return nil +} + +func mapWarehouseDTO(wh *entity.Warehouse) *RecordingWarehouseDTO { + if wh == nil || wh.Id == 0 { + return nil + } + dto := &RecordingWarehouseDTO{ + Id: wh.Id, + Name: wh.Name, + } + if wh.Area.Id != 0 { + dto.Area = &RecordingAreaDTO{ + Id: wh.Area.Id, + Name: wh.Area.Name, + } + } + if wh.Location != nil && wh.Location.Id != 0 { + dto.Location = &RecordingLocationDTO{ + Id: wh.Location.Id, + Name: wh.Location.Name, + Address: wh.Location.Address, + } + } + return dto +} + func mapProductWarehouseDTO(pw *entity.ProductWarehouse) productWarehouseDTO.ProductWarehouseDTO { if pw == nil { return productWarehouseDTO.ProductWarehouseDTO{} @@ -271,6 +380,20 @@ func mapProductWarehouseDTO(pw *entity.ProductWarehouse) productWarehouseDTO.Pro return *mapped } +func floatValue(value *float64) float64 { + if value == nil { + return 0 + } + return *value +} + +func intValue(value *int) int { + if value == nil { + return 0 + } + return *value +} + func defaultRecordingLatestApproval(e entity.Recording) approvalDTO.ApprovalRelationDTO { result := approvalDTO.ApprovalRelationDTO{} diff --git a/internal/modules/production/recordings/repositories/recording.repository.go b/internal/modules/production/recordings/repositories/recording.repository.go index 941d4507..dafd92ce 100644 --- a/internal/modules/production/recordings/repositories/recording.repository.go +++ b/internal/modules/production/recordings/repositories/recording.repository.go @@ -64,19 +64,28 @@ func (r *RecordingRepositoryImpl) WithRelations(db *gorm.DB) *gorm.DB { return db. Preload("CreatedUser"). Preload("ProjectFlockKandang"). + Preload("ProjectFlockKandang.Kandang"). Preload("ProjectFlockKandang.ProjectFlock"). + Preload("ProjectFlockKandang.ProjectFlock.ProductionStandard"). + Preload("ProjectFlockKandang.ProjectFlock.Fcr"). Preload("Depletions"). Preload("Depletions.ProductWarehouse"). Preload("Depletions.ProductWarehouse.Product"). Preload("Depletions.ProductWarehouse.Warehouse"). + Preload("Depletions.ProductWarehouse.Warehouse.Area"). + Preload("Depletions.ProductWarehouse.Warehouse.Location"). Preload("Stocks"). Preload("Stocks.ProductWarehouse"). Preload("Stocks.ProductWarehouse.Product"). Preload("Stocks.ProductWarehouse.Warehouse"). + Preload("Stocks.ProductWarehouse.Warehouse.Area"). + Preload("Stocks.ProductWarehouse.Warehouse.Location"). Preload("Eggs"). Preload("Eggs.ProductWarehouse"). Preload("Eggs.ProductWarehouse.Product"). - Preload("Eggs.ProductWarehouse.Warehouse") + Preload("Eggs.ProductWarehouse.Warehouse"). + Preload("Eggs.ProductWarehouse.Warehouse.Area"). + Preload("Eggs.ProductWarehouse.Warehouse.Location") } func (r *RecordingRepositoryImpl) GetLatestByProjectFlockKandangID(ctx context.Context, projectFlockKandangId uint) (*entity.Recording, error) { diff --git a/internal/modules/production/recordings/services/recording.service.go b/internal/modules/production/recordings/services/recording.service.go index 819552dc..18c00966 100644 --- a/internal/modules/production/recordings/services/recording.service.go +++ b/internal/modules/production/recordings/services/recording.service.go @@ -169,6 +169,14 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent } ctx := c.Context() + recordTime := time.Now().UTC() + if req.RecordDate != nil && strings.TrimSpace(*req.RecordDate) != "" { + parsed, err := time.Parse("2006-01-02", strings.TrimSpace(*req.RecordDate)) + if err != nil { + return nil, fiber.NewError(fiber.StatusBadRequest, "record_date must be in YYYY-MM-DD format") + } + recordTime = parsed.UTC() + } pfk, err := s.ProjectFlockKandangRepo.GetByID(ctx, req.ProjectFlockKandangId) if err != nil { @@ -188,6 +196,9 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent if err := s.ensureChickInExists(ctx, pfk.Id); err != nil { return nil, err } + if err := s.ensureProductionStandardWeekStart(ctx, pfk); err != nil { + return nil, err + } if !isLaying && len(req.Eggs) > 0 { return nil, fiber.NewError(fiber.StatusBadRequest, "Egg details permitted only for laying project flocks") @@ -211,7 +222,6 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent return err } - recordTime := time.Now().UTC() existsToday, err := s.Repository.ExistsOnDate(ctx, req.ProjectFlockKandangId, recordTime) if err != nil { s.Log.Errorf("Failed to verify existing recording on date: %+v", err) @@ -1330,12 +1340,16 @@ func (s *recordingService) attachProductionStandard(ctx context.Context, item *e return nil } - week := ((int(*item.Day) - 1) / 7) + 1 + category := strings.ToUpper(item.ProjectFlockKandang.ProjectFlock.Category) + weekBase := 1 + if category == string(utils.ProjectFlockCategoryLaying) { + weekBase = 18 + } + week := ((int(*item.Day) - 1) / 7) + weekBase if week <= 0 { return nil } - category := strings.ToUpper(item.ProjectFlockKandang.ProjectFlock.Category) db := s.Repository.DB() standardDetailRepo := rProductionStandard.NewProductionStandardDetailRepository(db) growthDetailRepo := rProductionStandard.NewStandardGrowthDetailRepository(db) @@ -1462,3 +1476,46 @@ func (s *recordingService) ensureChickInExists(ctx context.Context, projectFlock return fiber.NewError(fiber.StatusBadRequest, "Chick in project flock belum disetujui sehingga belum dapat membuat recording") } + +func (s *recordingService) ensureProductionStandardWeekStart(ctx context.Context, pfk *entity.ProjectFlockKandang) error { + if pfk == nil || pfk.ProjectFlock.Id == 0 { + return nil + } + + standardID := pfk.ProjectFlock.ProductionStandardId + if standardID == 0 { + return nil + } + + category := strings.ToUpper(pfk.ProjectFlock.Category) + switch category { + case string(utils.ProjectFlockCategoryLaying): + detailRepo := rProductionStandard.NewProductionStandardDetailRepository(s.Repository.DB()) + details, err := detailRepo.GetByProductionStandardID(ctx, standardID) + if err != nil { + return err + } + startWeek := 0 + if len(details) > 0 { + startWeek = details[0].Week + } + if startWeek != 18 { + return fiber.NewError(fiber.StatusBadRequest, "Week tidak sesuai dengan standart kategori project flock") + } + case string(utils.ProjectFlockCategoryGrowing): + growthRepo := rProductionStandard.NewStandardGrowthDetailRepository(s.Repository.DB()) + details, err := growthRepo.GetByProductionStandardID(ctx, standardID) + if err != nil { + return err + } + startWeek := 0 + if len(details) > 0 { + startWeek = details[0].Week + } + if startWeek != 1 { + return fiber.NewError(fiber.StatusBadRequest, "Week tidak sesuai dengan standart kategori project flock") + } + } + + return nil +} diff --git a/internal/modules/production/recordings/validations/recording.validation.go b/internal/modules/production/recordings/validations/recording.validation.go index a1d6aaf7..8b4eab57 100644 --- a/internal/modules/production/recordings/validations/recording.validation.go +++ b/internal/modules/production/recordings/validations/recording.validation.go @@ -21,6 +21,7 @@ type ( type Create struct { ProjectFlockKandangId uint `json:"project_flock_kandang_id" validate:"required,number,min=1"` + RecordDate *string `json:"record_date,omitempty" validate:"omitempty,datetime=2006-01-02"` Stocks []Stock `json:"stocks" validate:"dive"` Depletions []Depletion `json:"depletions" validate:"dive"` Eggs []Egg `json:"eggs" validate:"omitempty,dive"`