diff --git a/internal/entities/recording.go b/internal/entities/recording.go index 0cc5dc03..1ca3deb2 100644 --- a/internal/entities/recording.go +++ b/internal/entities/recording.go @@ -12,7 +12,9 @@ type Recording struct { RecordDatetime time.Time `gorm:"column:record_datetime;not null"` Day *int `gorm:"column:day"` TotalDepletionQty *float64 `gorm:"column:total_depletion_qty"` + TotalDepletionCumQty *float64 `gorm:"-"` CumDepletionRate *float64 `gorm:"column:cum_depletion_rate"` + DepletionRate *float64 `gorm:"-"` CumIntake *int `gorm:"column:cum_intake"` FcrValue *float64 `gorm:"column:fcr_value"` TotalChickQty *float64 `gorm:"column:total_chick_qty"` diff --git a/internal/modules/production/recordings/dto/recording.dto.go b/internal/modules/production/recordings/dto/recording.dto.go index 0fa14e97..191b9676 100644 --- a/internal/modules/production/recordings/dto/recording.dto.go +++ b/internal/modules/production/recordings/dto/recording.dto.go @@ -1,6 +1,7 @@ package dto import ( + "math" "strings" "time" @@ -73,7 +74,9 @@ type RecordingRelationDTO struct { RecordDatetime time.Time `json:"record_datetime"` Day int `json:"day"` TotalDepletionQty float64 `json:"total_depletion_qty"` + TotalDepletionCumQty float64 `json:"total_depletion_cum_qty"` CumDepletionRate float64 `json:"cum_depletion_rate"` + DepletionRate float64 `json:"depletion_rate"` CumIntake int `json:"cum_intake"` FcrValue float64 `json:"fcr_value"` HenDay float64 `json:"hen_day"` @@ -230,7 +233,9 @@ func toRecordingRelationDTO(e entity.Recording) RecordingRelationDTO { RecordDatetime: e.RecordDatetime, Day: intValue(e.Day), TotalDepletionQty: floatValue(e.TotalDepletionQty), - CumDepletionRate: floatValue(e.CumDepletionRate), + TotalDepletionCumQty: floatValue(e.TotalDepletionCumQty), + CumDepletionRate: roundFloatValue(e.CumDepletionRate, 2), + DepletionRate: roundFloatValue(e.DepletionRate, 2), CumIntake: intValue(e.CumIntake), FcrValue: floatValue(e.FcrValue), HenDay: floatValue(e.HenDay), @@ -426,6 +431,17 @@ func floatValue(value *float64) float64 { return *value } +func roundFloatValue(value *float64, places int) float64 { + if value == nil { + return 0 + } + if places <= 0 { + return math.Round(*value) + } + factor := math.Pow(10, float64(places)) + return math.Round(*value*factor) / factor +} + func intValue(value *int) int { if value == nil { return 0 diff --git a/internal/modules/production/recordings/repositories/recording.repository.go b/internal/modules/production/recordings/repositories/recording.repository.go index 27c399f4..ce4dc0df 100644 --- a/internal/modules/production/recordings/repositories/recording.repository.go +++ b/internal/modules/production/recordings/repositories/recording.repository.go @@ -39,6 +39,7 @@ type RecordingRepository interface { ExistsOnDate(ctx context.Context, projectFlockKandangId uint, recordTime time.Time) (bool, error) SumRecordingDepletions(tx *gorm.DB, recordingID uint) (float64, error) + GetCumulativeDepletionByProjectFlockKandangUntil(tx *gorm.DB, projectFlockKandangId uint, recordTime time.Time) (float64, error) FindPreviousRecording(tx *gorm.DB, projectFlockKandangId uint, currentDay int) (*entity.Recording, error) GetTotalChick(tx *gorm.DB, projectFlockKandangId uint) (int64, error) GetTotalChickinByProjectFlockKandang(tx *gorm.DB, projectFlockKandangId uint) (float64, error) @@ -314,6 +315,23 @@ func (r *RecordingRepositoryImpl) SumRecordingDepletions(tx *gorm.DB, recordingI return result, nil } +func (r *RecordingRepositoryImpl) GetCumulativeDepletionByProjectFlockKandangUntil(tx *gorm.DB, projectFlockKandangId uint, recordTime time.Time) (float64, error) { + if projectFlockKandangId == 0 || recordTime.IsZero() { + return 0, nil + } + + var total float64 + err := tx. + Table("recording_depletions rd"). + Select("COALESCE(SUM(rd.qty),0)"). + Joins("JOIN recordings r ON r.id = rd.recording_id"). + Where("r.project_flock_kandangs_id = ?", projectFlockKandangId). + Where("r.record_datetime <= ?", recordTime). + Where("r.deleted_at IS NULL"). + Scan(&total).Error + return total, err +} + func (r *RecordingRepositoryImpl) FindPreviousRecording(tx *gorm.DB, projectFlockKandangId uint, currentDay int) (*entity.Recording, error) { if currentDay <= 1 { return nil, nil diff --git a/internal/modules/production/recordings/services/recording.service.go b/internal/modules/production/recordings/services/recording.service.go index 29f9cffc..28329041 100644 --- a/internal/modules/production/recordings/services/recording.service.go +++ b/internal/modules/production/recordings/services/recording.service.go @@ -128,6 +128,12 @@ func (s recordingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti if err := s.attachProductionStandards(c.Context(), recordings); err != nil { return nil, 0, err } + if err := s.attachCumulativeDepletions(c.Context(), recordings); err != nil { + return nil, 0, err + } + if err := s.attachDepletionRates(c.Context(), recordings); err != nil { + return nil, 0, err + } return recordings, total, nil } @@ -152,6 +158,12 @@ func (s recordingService) GetOne(c *fiber.Ctx, id uint) (*entity.Recording, erro if err := s.attachProductionStandard(c.Context(), recording); err != nil { return nil, err } + if err := s.attachCumulativeDepletion(c.Context(), recording); err != nil { + return nil, err + } + if err := s.attachDepletionRate(c.Context(), recording); err != nil { + return nil, err + } return recording, nil } @@ -1026,12 +1038,8 @@ func (s *recordingService) computeAndUpdateMetrics(ctx context.Context, tx *gorm return fmt.Errorf("getPreviousRecording: %w", err) } - var prevCumDepletionQty float64 var prevCumIntake float64 if prevRecording != nil { - if prevRecording.TotalDepletionQty != nil { - prevCumDepletionQty = *prevRecording.TotalDepletionQty - } if prevRecording.CumIntake != nil { prevCumIntake = float64(*prevRecording.CumIntake) } @@ -1063,12 +1071,16 @@ func (s *recordingService) computeAndUpdateMetrics(ctx context.Context, tx *gorm } currentDepletion := float64(totalDepletionQty) - cumDepletionQty := prevCumDepletionQty + currentDepletion + cumDepletionQty, err := s.Repository.GetCumulativeDepletionByProjectFlockKandangUntil(tx, recording.ProjectFlockKandangId, recording.RecordDatetime) + if err != nil { + return fmt.Errorf("getCumulativeDepletionByProjectFlockKandangUntil: %w", err) + } updates := map[string]any{ - "total_depletion_qty": cumDepletionQty, + "total_depletion_qty": currentDepletion, } - recording.TotalDepletionQty = &cumDepletionQty + recording.TotalDepletionQty = ¤tDepletion + recording.TotalDepletionCumQty = &cumDepletionQty var remainingChick float64 if totalChick > 0 { @@ -1111,6 +1123,9 @@ func (s *recordingService) computeAndUpdateMetrics(ctx context.Context, tx *gorm recording.CumDepletionRate = nil } + depletionRate := computeDepletionRate(prevRecording, currentDepletion, totalChick) + recording.DepletionRate = &depletionRate + var feedIntake float64 if remainingChick > 0 && usageInGrams > 0 { feedIntake = (usageInGrams / remainingChick) * 1000 @@ -1201,6 +1216,81 @@ func (s *recordingService) computeAndUpdateMetrics(ctx context.Context, tx *gorm return nil } +func computeDepletionRate(prevRecording *entity.Recording, currentDepletion float64, totalChick int64) float64 { + base := 0.0 + if prevRecording != nil && prevRecording.TotalChickQty != nil && *prevRecording.TotalChickQty > 0 { + base = *prevRecording.TotalChickQty + } else if totalChick > 0 { + // totalChick is already remaining after today's depletion; add back current to approximate previous population. + base = float64(totalChick) + currentDepletion + } + if base <= 0 { + return 0 + } + return (currentDepletion / base) * 100 +} + +func (s *recordingService) attachCumulativeDepletion(ctx context.Context, recording *entity.Recording) error { + if recording == nil || recording.ProjectFlockKandangId == 0 || recording.RecordDatetime.IsZero() { + return nil + } + total, err := s.Repository.GetCumulativeDepletionByProjectFlockKandangUntil(s.Repository.DB().WithContext(ctx), recording.ProjectFlockKandangId, recording.RecordDatetime) + if err != nil { + return err + } + recording.TotalDepletionCumQty = &total + return nil +} + +func (s *recordingService) attachCumulativeDepletions(ctx context.Context, recordings []entity.Recording) error { + if len(recordings) == 0 { + return nil + } + for i := range recordings { + if err := s.attachCumulativeDepletion(ctx, &recordings[i]); err != nil { + return err + } + } + return nil +} + +func (s *recordingService) attachDepletionRate(ctx context.Context, recording *entity.Recording) error { + if recording == nil { + return nil + } + current := 0.0 + if recording.TotalDepletionQty != nil { + current = *recording.TotalDepletionQty + } + day := 0 + if recording.Day != nil { + day = *recording.Day + } + prev, err := s.Repository.FindPreviousRecording(s.Repository.DB().WithContext(ctx), recording.ProjectFlockKandangId, day) + if err != nil { + return err + } + totalChick, err := s.Repository.GetTotalChick(s.Repository.DB().WithContext(ctx), recording.ProjectFlockKandangId) + if err != nil { + return err + } + rate := computeDepletionRate(prev, current, totalChick) + recording.DepletionRate = &rate + return nil +} + +func (s *recordingService) attachDepletionRates(ctx context.Context, recordings []entity.Recording) error { + if len(recordings) == 0 { + return nil + } + for i := range recordings { + if err := s.attachDepletionRate(ctx, &recordings[i]); err != nil { + return err + } + } + return nil +} + func (s *recordingService) createRecordingApproval( ctx context.Context, db *gorm.DB,