package service import ( "context" "errors" "fmt" "math" "sort" "strconv" "strings" "time" commonService "gitlab.com/mbugroup/lti-api.git/internal/common/service" "gitlab.com/mbugroup/lti-api.git/internal/modules/dashboards/dto" repository "gitlab.com/mbugroup/lti-api.git/internal/modules/dashboards/repositories" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/dashboards/validations" "gitlab.com/mbugroup/lti-api.git/internal/utils" "github.com/go-playground/validator/v10" "github.com/sirupsen/logrus" "gorm.io/gorm" ) type DashboardService interface { GetAll(ctx context.Context, params *validation.Query) (dto.DashboardPerformanceOverviewDTO, int64, error) DB() *gorm.DB } type dashboardService struct { Log *logrus.Logger Validate *validator.Validate Repository repository.DashboardRepository HppSvc commonService.HppService } func NewDashboardService(repo repository.DashboardRepository, validate *validator.Validate, hppSvc commonService.HppService) DashboardService { return &dashboardService{ Log: utils.Log, Validate: validate, Repository: repo, HppSvc: hppSvc, } } func (s dashboardService) DB() *gorm.DB { return s.Repository.DB() } func (s dashboardService) GetAll(ctx context.Context, params *validation.Query) (dto.DashboardPerformanceOverviewDTO, int64, error) { if err := s.Validate.Struct(params); err != nil { return dto.DashboardPerformanceOverviewDTO{}, 0, err } filter := &validation.DashboardFilter{ LokasiIds: params.LokasiIds, FlockIds: params.FlockIds, KandangIds: params.KandangIds, } statistics, err := s.buildPerformanceStatistics(ctx, params, filter) if err != nil { return dto.DashboardPerformanceOverviewDTO{}, 0, err } charts, err := s.buildPerformanceCharts(ctx, params, filter) if err != nil { return dto.DashboardPerformanceOverviewDTO{}, 0, err } response := dto.DashboardPerformanceOverviewDTO{ StatisticsData: statistics, Charts: charts, } if len(params.Include) > 0 { include := map[string]bool{} for _, item := range params.Include { include[item] = true } if !include["statistics"] { response.StatisticsData = []dto.DashboardStatisticsDTO{} } if !include["charts"] { response.Charts = map[string]dto.DashboardChartDTO{} } } if response.StatisticsData == nil { response.StatisticsData = []dto.DashboardStatisticsDTO{} } if response.Charts == nil { response.Charts = map[string]dto.DashboardChartDTO{} } return response, 1, nil } func (s dashboardService) buildPerformanceStatistics(ctx context.Context, params *validation.Query, filter *validation.DashboardFilter) ([]dto.DashboardStatisticsDTO, error) { location, err := time.LoadLocation("Asia/Jakarta") if err != nil { return nil, fmt.Errorf("failed to load timezone configuration: %w", err) } if params.PeriodStart.IsZero() || params.PeriodEnd.IsZero() || params.PeriodEndExclusive.IsZero() { return nil, errors.New("period dates are not initialized") } startDate := params.PeriodStart endDate := params.PeriodEnd endExclusive := params.PeriodEndExclusive globalStartDate, globalEndDate, globalEndExclusive := currentPeriodDates(location) hppCurrent, hppLast, err := s.calculateHppGlobal(ctx, globalStartDate, globalEndExclusive, globalEndDate, location) if err != nil { return nil, err } sellingCurrent, sellingLast, err := s.calculateSellingPrice(ctx, globalEndDate, location) if err != nil { return nil, err } hasFilter := filter != nil && (len(filter.LokasiIds) > 0 || len(filter.FlockIds) > 0 || len(filter.KandangIds) > 0) fcrCurrent := 0.0 fcrLast := 0.0 mortalityCurrent := 0.0 mortalityLast := 0.0 if hasFilter { fcrCurrent, fcrLast, err = s.calculateFcr(ctx, filter, startDate, endExclusive, endDate, location) if err != nil { return nil, err } mortalityCurrent, mortalityLast, err = s.calculateMortality(ctx, filter, startDate, endExclusive, endDate, location) if err != nil { return nil, err } } hppPercent := percentDelta(hppCurrent, hppLast) sellingPercent := percentDelta(sellingCurrent, sellingLast) stats := []dto.DashboardStatisticsDTO{ { Label: "HPP Global", Value: roundTo(hppCurrent, 0), PercentLastMonth: roundTo(hppPercent*100, 2), }, { Label: "Avg. Selling Price", Value: roundTo(sellingCurrent, 0), PercentLastMonth: roundTo(sellingPercent*100, 2), }, } if hasFilter { fcrPercent := percentDelta(fcrCurrent, fcrLast) mortalityPercent := percentDelta(mortalityCurrent, mortalityLast) stats = append(stats, dto.DashboardStatisticsDTO{ Label: "FCR", Value: roundTo(fcrCurrent, 2), PercentLastMonth: roundTo(fcrPercent*100, 2), }, dto.DashboardStatisticsDTO{ Label: "Mortality", Value: roundTo(mortalityCurrent, 2), PercentLastMonth: roundTo(mortalityPercent*100, 2), }, ) } return stats, nil } func (s dashboardService) buildPerformanceCharts(ctx context.Context, params *validation.Query, filter *validation.DashboardFilter) (map[string]dto.DashboardChartDTO, error) { if params.AnalysisMode == validation.AnalysisModeComparison { return s.buildComparisonCharts(ctx, params, filter) } if params.PeriodStart.IsZero() || params.PeriodEndExclusive.IsZero() { return nil, errors.New("period dates are not initialized") } if filter == nil || (len(filter.LokasiIds) == 0 && len(filter.FlockIds) == 0 && len(filter.KandangIds) == 0) { return map[string]dto.DashboardChartDTO{}, nil } startDate := params.PeriodStart endExclusive := params.PeriodEndExclusive recordings, err := s.Repository.GetRecordingWeeklyMetrics(ctx, startDate, endExclusive, filter) if err != nil { return nil, err } uniformities, err := s.Repository.GetUniformityWeeklyMetrics(ctx, startDate, endExclusive, filter) if err != nil { return nil, err } weekSet := map[int]struct{}{} for _, row := range recordings { if row.Week > 0 { weekSet[row.Week] = struct{}{} } } for _, row := range uniformities { if row.Week > 0 { weekSet[row.Week] = struct{}{} } } weeks := make([]int, 0, len(weekSet)) for week := range weekSet { weeks = append(weeks, week) } sort.Ints(weeks) standards, err := s.Repository.GetStandardWeeklyMetrics(ctx, weeks, filter) if err != nil { return nil, err } standardFcr, err := s.Repository.GetStandardFcrWeekly(ctx, weeks, filter) if err != nil { return nil, err } recordingMap := map[int]repository.RecordingWeeklyMetric{} for _, row := range recordings { recordingMap[row.Week] = row } uniformityMap := map[int]repository.UniformityWeeklyMetric{} for _, row := range uniformities { uniformityMap[row.Week] = row } standardMap := map[int]repository.StandardWeeklyMetric{} for _, row := range standards { standardMap[row.Week] = row } standardFcrMap := map[int]float64{} for _, row := range standardFcr { standardFcrMap[row.Week] = row.StdFcr } weeklyEggs, err := s.Repository.GetEggWeightWeeklyGrams(ctx, startDate, endExclusive, filter) if err != nil { return nil, err } weeklyFeedRows, err := s.Repository.GetFeedUsageWeeklyByUom(ctx, startDate, endExclusive, filter) if err != nil { return nil, err } weeklyEggMap := map[int]float64{} for _, row := range weeklyEggs { weeklyEggMap[row.Week] = row.EggWeightGrams } weeklyFeedMap := map[int]float64{} for _, row := range weeklyFeedRows { weeklyFeedMap[row.Week] += feedUsageRowToGrams(row.TotalQty, row.UomName) } bodyWeightDataset := make([]map[string]interface{}, 0, len(weeks)) bodyWeightDatasetIndexByWeek := make(map[int]int, len(weeks)) performanceDataset := make([]map[string]interface{}, 0, len(weeks)) fcrDataset := make([]map[string]interface{}, 0, len(weeks)) deplesiDataset := make([]map[string]interface{}, 0, len(weeks)) qualityDataset := make([]map[string]interface{}, 0, len(weeks)) cumEgg := 0.0 cumFeed := 0.0 for _, week := range weeks { rec, hasRec := recordingMap[week] uni, hasUni := uniformityMap[week] std, hasStd := standardMap[week] stdFcr, hasStdFcr := standardFcrMap[week] weekEgg := weeklyEggMap[week] weekFeed := weeklyFeedMap[week] actFcr := 0.0 if weekEgg > 0 { actFcr = weekFeed / weekEgg } cumEgg += weekEgg cumFeed += weekFeed actFcrCum := 0.0 if cumEgg > 0 { actFcrCum = cumFeed / cumEgg } bodyWeightRow := map[string]interface{}{ "week": week, } if hasUni { bodyWeightRow["body_weight"] = roundTo(uni.AverageWeight, 2) } if hasStd { bodyWeightRow["std_body_weight"] = roundTo(std.StdBodyWeight, 2) } if len(bodyWeightRow) > 1 { bodyWeightDataset = append(bodyWeightDataset, bodyWeightRow) } performanceRow := map[string]interface{}{ "week": week, } if hasRec { performanceRow["act_laying"] = roundTo(rec.HenDay, 2) performanceRow["act_egg_weight"] = roundTo(rec.EggWeight, 2) performanceRow["act_feed_intake"] = roundTo(rec.FeedIntake, 2) } if hasUni { performanceRow["act_uniformity"] = roundTo(uni.Uniformity, 2) } if hasStd { performanceRow["std_laying"] = roundTo(std.StdLaying, 2) performanceRow["std_egg_weight"] = roundTo(std.StdEggWeight, 2) performanceRow["std_feed_intake"] = roundTo(std.StdFeedIntake, 2) performanceRow["std_uniformity"] = roundTo(std.StdUniformity, 2) } if len(performanceRow) > 1 { performanceDataset = append(performanceDataset, performanceRow) } fcrRow := map[string]interface{}{ "week": week, } if weekEgg > 0 && weekFeed > 0 { fcrRow["act_fcr"] = roundTo(actFcr, 2) } if cumEgg > 0 && cumFeed > 0 { fcrRow["act_fcr_cum"] = roundTo(actFcrCum, 2) } if hasStdFcr { fcrRow["std_fcr"] = roundTo(stdFcr, 2) fcrRow["std_fcr_cum"] = roundTo(stdFcr, 2) } if len(fcrRow) > 1 { fcrDataset = append(fcrDataset, fcrRow) } deplesiRow := map[string]interface{}{ "week": week, } if hasRec { deplesiRow["act_deplesi"] = roundTo(rec.CumDepletionRate, 2) } if hasStd { deplesiRow["std_deplesi"] = roundTo(std.StdDepletion, 2) } if len(deplesiRow) > 1 { deplesiDataset = append(deplesiDataset, deplesiRow) } } bodyWeightDataset = extendBodyWeightDatasetUntilEndDate( bodyWeightDataset, bodyWeightDatasetIndexByWeek, uniformities, uniformityMap, standardMap, params.PeriodEnd, ) qualityRows, err := s.Repository.GetEggQualityWeeklyMetrics(ctx, startDate, endExclusive, filter) if err != nil { return nil, err } for _, row := range qualityRows { normalPercent := 0.0 abnormalPercent := 0.0 if row.TotalQty > 0 { normalPercent = (row.NormalQty / row.TotalQty) * 100 abnormalPercent = (row.AbnormalQty / row.TotalQty) * 100 } qualityDataset = append(qualityDataset, map[string]interface{}{ "week": row.Week, "normal": roundTo(normalPercent, 2), "abnormal": roundTo(abnormalPercent, 2), }) } charts := map[string]dto.DashboardChartDTO{ "body_weight": { Series: []dto.DashboardChartSeriesDTO{ {Id: "body_weight", Label: "Body Weight", Unit: "g"}, {Id: "std_body_weight", Label: "STD. Body Weight", Unit: "g"}, }, Dataset: bodyWeightDataset, }, "performance": { Series: []dto.DashboardChartSeriesDTO{ {Id: "act_laying", Label: "Act. % Laying", Unit: "%"}, {Id: "std_laying", Label: "STD. % Laying", Unit: "%"}, {Id: "act_egg_weight", Label: "Act. Egg Weight", Unit: "%"}, {Id: "std_egg_weight", Label: "STD. Egg Weight", Unit: "%"}, {Id: "act_feed_intake", Label: "Act. Feed Intake", Unit: "%"}, {Id: "std_feed_intake", Label: "STD. Feed Intake", Unit: "%"}, {Id: "act_uniformity", Label: "Act. Uniformity", Unit: "%"}, {Id: "std_uniformity", Label: "STD. Uniformity", Unit: "%"}, }, Dataset: performanceDataset, }, "fcr": { Series: []dto.DashboardChartSeriesDTO{ {Id: "act_fcr", Label: "Act. FCR", Unit: "kg/kg"}, {Id: "std_fcr", Label: "STD. FCR", Unit: "kg/kg"}, {Id: "act_fcr_cum", Label: "Act. FCR Cummulative", Unit: "kg/kg"}, {Id: "std_fcr_cum", Label: "STD. FCR Cummulative", Unit: "kg/kg"}, }, Dataset: fcrDataset, }, "deplesi": { Series: []dto.DashboardChartSeriesDTO{ {Id: "act_deplesi", Label: "Act. Deplesi", Unit: "%"}, {Id: "std_deplesi", Label: "STD. Deplesi", Unit: "%"}, }, Dataset: deplesiDataset, }, "quality_control": { Series: []dto.DashboardChartSeriesDTO{ {Id: "normal", Label: "Normal", Unit: "%"}, {Id: "abnormal", Label: "Abnormal", Unit: "%"}, }, Dataset: qualityDataset, }, } return charts, nil } func (s dashboardService) buildComparisonCharts(ctx context.Context, params *validation.Query, filter *validation.DashboardFilter) (map[string]dto.DashboardChartDTO, error) { if params.PeriodStart.IsZero() || params.PeriodEndExclusive.IsZero() { return nil, errors.New("period dates are not initialized") } startDate := params.PeriodStart endExclusive := params.PeriodEndExclusive metric := strings.ToLower(strings.TrimSpace(params.Metric)) if metric == "" { return s.buildComparisonChartsAll(ctx, startDate, endExclusive, params, filter) } seriesRows, err := s.Repository.GetComparisonSeries(ctx, startDate, endExclusive, filter, params.ComparisonType) if err != nil { return nil, err } metricRows, err := s.Repository.GetComparisonWeeklyMetrics(ctx, startDate, endExclusive, filter, params.ComparisonType, metric) if err != nil { return nil, err } weeks, actualMap := mapComparisonWeeklyMetricRows(metricRows) if len(weeks) == 0 { return map[string]dto.DashboardChartDTO{}, nil } standardMap, err := s.standardComparisonMap(ctx, weeks, metric, filter) if err != nil { return nil, err } chart := buildComparisonPercentChart(seriesRows, weeks, actualMap, standardMap) return map[string]dto.DashboardChartDTO{ strings.ToLower(params.ComparisonType): chart, }, nil } func (s dashboardService) buildComparisonChartsAll(ctx context.Context, startDate, endExclusive time.Time, params *validation.Query, filter *validation.DashboardFilter) (map[string]dto.DashboardChartDTO, error) { seriesRows, err := s.Repository.GetComparisonSeries(ctx, startDate, endExclusive, filter, params.ComparisonType) if err != nil { return nil, err } layingRows, err := s.Repository.GetComparisonWeeklyMetrics(ctx, startDate, endExclusive, filter, params.ComparisonType, validation.MetricLaying) if err != nil { return nil, err } eggWeightRows, err := s.Repository.GetComparisonWeeklyMetrics(ctx, startDate, endExclusive, filter, params.ComparisonType, validation.MetricEggWeight) if err != nil { return nil, err } feedIntakeRows, err := s.Repository.GetComparisonWeeklyMetrics(ctx, startDate, endExclusive, filter, params.ComparisonType, validation.MetricFeedIntake) if err != nil { return nil, err } fcrRows, err := s.Repository.GetComparisonWeeklyMetrics(ctx, startDate, endExclusive, filter, params.ComparisonType, validation.MetricFcr) if err != nil { return nil, err } deplesiRows, err := s.Repository.GetComparisonWeeklyMetrics(ctx, startDate, endExclusive, filter, params.ComparisonType, validation.MetricMortality) if err != nil { return nil, err } uniformityRows, err := s.Repository.GetComparisonWeeklyUniformityMetrics(ctx, startDate, endExclusive, filter, params.ComparisonType) if err != nil { return nil, err } weeks := mergeComparisonWeeks( layingRows, eggWeightRows, feedIntakeRows, fcrRows, deplesiRows, uniformityRows, ) if len(weeks) == 0 { return map[string]dto.DashboardChartDTO{}, nil } standards, err := s.Repository.GetStandardWeeklyMetrics(ctx, weeks, filter) if err != nil { return nil, err } standardFcr, err := s.Repository.GetStandardFcrWeekly(ctx, weeks, filter) if err != nil { return nil, err } stdBodyWeight := map[int]float64{} stdLaying := map[int]float64{} stdEggWeight := map[int]float64{} stdFeedIntake := map[int]float64{} stdUniformity := map[int]float64{} stdDeplesi := map[int]float64{} for _, row := range standards { stdBodyWeight[row.Week] = row.StdBodyWeight stdLaying[row.Week] = row.StdLaying stdEggWeight[row.Week] = row.StdEggWeight stdFeedIntake[row.Week] = row.StdFeedIntake stdUniformity[row.Week] = row.StdUniformity stdDeplesi[row.Week] = row.StdDepletion } stdFcr := map[int]float64{} for _, row := range standardFcr { stdFcr[row.Week] = row.StdFcr } _, layingActual := mapComparisonWeeklyMetricRows(layingRows) _, eggWeightActual := mapComparisonWeeklyMetricRows(eggWeightRows) _, feedActual := mapComparisonWeeklyMetricRows(feedIntakeRows) _, fcrActual := mapComparisonWeeklyMetricRows(fcrRows) _, deplesiActual := mapComparisonWeeklyMetricRows(deplesiRows) _, bodyWeightActual, _, uniformityActual := mapComparisonUniformityRows(uniformityRows) aggregateActual := buildAggregateComparisonPercent(weeks, seriesRows, aggregateComparisonInput{ BodyWeightActual: bodyWeightActual, LayingActual: layingActual, EggWeightActual: eggWeightActual, FeedIntakeActual: feedActual, UniformityActual: uniformityActual, FcrActual: fcrActual, DeplesiActual: deplesiActual, StdBodyWeight: stdBodyWeight, StdLaying: stdLaying, StdEggWeight: stdEggWeight, StdFeedIntake: stdFeedIntake, StdUniformity: stdUniformity, StdFcr: stdFcr, StdDeplesi: stdDeplesi, }) if len(aggregateActual) == 0 { return map[string]dto.DashboardChartDTO{}, nil } chartKey := strings.ToLower(params.ComparisonType) return map[string]dto.DashboardChartDTO{ chartKey: buildComparisonAggregateChart(seriesRows, weeks, aggregateActual), }, nil } type aggregateComparisonInput struct { BodyWeightActual map[int]map[uint]float64 LayingActual map[int]map[uint]float64 EggWeightActual map[int]map[uint]float64 FeedIntakeActual map[int]map[uint]float64 UniformityActual map[int]map[uint]float64 FcrActual map[int]map[uint]float64 DeplesiActual map[int]map[uint]float64 StdBodyWeight map[int]float64 StdLaying map[int]float64 StdEggWeight map[int]float64 StdFeedIntake map[int]float64 StdUniformity map[int]float64 StdFcr map[int]float64 StdDeplesi map[int]float64 } func buildAggregateComparisonPercent(weeks []int, seriesRows []repository.ComparisonSeries, input aggregateComparisonInput) map[int]map[uint]float64 { result := map[int]map[uint]float64{} for _, week := range weeks { stdBodyWeight := input.StdBodyWeight[week] stdLaying := input.StdLaying[week] stdEggWeight := input.StdEggWeight[week] stdFeedIntake := input.StdFeedIntake[week] stdUniformity := input.StdUniformity[week] stdFcr := input.StdFcr[week] stdDeplesi := input.StdDeplesi[week] for _, series := range seriesRows { sum := 0.0 count := 0.0 if percent, ok := higherIsBetterPercent(input.LayingActual, week, series.Id, stdLaying); ok { sum += percent count++ } if percent, ok := higherIsBetterPercent(input.EggWeightActual, week, series.Id, stdEggWeight); ok { sum += percent count++ } if percent, ok := higherIsBetterPercent(input.UniformityActual, week, series.Id, stdUniformity); ok { sum += percent count++ } if percent, ok := lowerIsBetterPercent(input.FcrActual, week, series.Id, stdFcr); ok { sum += percent count++ } if percent, ok := lowerIsBetterPercent(input.DeplesiActual, week, series.Id, stdDeplesi); ok { sum += percent count++ } if percent, ok := higherIsBetterPercent(input.BodyWeightActual, week, series.Id, stdBodyWeight); ok { sum += percent count++ } if percent, ok := lowerIsBetterPercent(input.FeedIntakeActual, week, series.Id, stdFeedIntake); ok { sum += percent count++ } if result[week] == nil { result[week] = map[uint]float64{} } if count == 0 { result[week][series.Id] = 0 continue } result[week][series.Id] = sum / count } } return result } func higherIsBetterPercent(actual map[int]map[uint]float64, week int, seriesId uint, standard float64) (float64, bool) { if standard <= 0 { return 0, false } val, ok := metricValue(actual, week, seriesId) if !ok || val <= 0 { return 0, false } return clampPercent((val / standard) * 100), true } func lowerIsBetterPercent(actual map[int]map[uint]float64, week int, seriesId uint, standard float64) (float64, bool) { if standard <= 0 { return 0, false } val, ok := metricValue(actual, week, seriesId) if !ok || val <= 0 { return 0, false } return clampPercent((standard / val) * 100), true } func metricValue(actual map[int]map[uint]float64, week int, seriesId uint) (float64, bool) { weekRows, ok := actual[week] if !ok { return 0, false } val, ok := weekRows[seriesId] return val, ok } func clampPercent(value float64) float64 { if value < 0 { return 0 } if value > 200 { return 200 } return value } func buildComparisonAggregateChart(seriesRows []repository.ComparisonSeries, weeks []int, actual map[int]map[uint]float64) dto.DashboardChartDTO { series := make([]dto.DashboardChartSeriesDTO, 0, len(seriesRows)) for _, sRow := range seriesRows { series = append(series, dto.DashboardChartSeriesDTO{ Id: strconv.FormatUint(uint64(sRow.Id), 10), Label: sRow.Label, Unit: "%", }) } dataset := make([]map[string]interface{}, 0, len(weeks)) for _, week := range weeks { row := map[string]interface{}{ "week": week, } values, ok := actual[week] if !ok { continue } for _, sRow := range seriesRows { if val, exists := values[sRow.Id]; exists { row[strconv.FormatUint(uint64(sRow.Id), 10)] = roundTo(val, 2) } } if len(row) > 1 { dataset = append(dataset, row) } } return dto.DashboardChartDTO{ Series: series, Dataset: dataset, } } func (s dashboardService) standardComparisonMap(ctx context.Context, weeks []int, metric string, filter *validation.DashboardFilter) (map[int]float64, error) { switch metric { case validation.MetricFcr: rows, err := s.Repository.GetStandardFcrWeekly(ctx, weeks, filter) if err != nil { return nil, err } result := map[int]float64{} for _, row := range rows { result[row.Week] = row.StdFcr } return result, nil case validation.MetricLaying, validation.MetricEggWeight, validation.MetricFeedIntake, validation.MetricMortality: rows, err := s.Repository.GetStandardWeeklyMetrics(ctx, weeks, filter) if err != nil { return nil, err } result := map[int]float64{} for _, row := range rows { switch metric { case validation.MetricLaying: result[row.Week] = row.StdLaying case validation.MetricEggWeight: result[row.Week] = row.StdEggWeight case validation.MetricFeedIntake: result[row.Week] = row.StdFeedIntake case validation.MetricMortality: result[row.Week] = row.StdDepletion } } return result, nil default: return map[int]float64{}, nil } } func mapComparisonWeeklyMetricRows(rows []repository.ComparisonWeeklyMetric) ([]int, map[int]map[uint]float64) { weekSet := map[int]struct{}{} values := map[int]map[uint]float64{} for _, row := range rows { if row.Week <= 0 { continue } weekSet[row.Week] = struct{}{} if values[row.Week] == nil { values[row.Week] = map[uint]float64{} } values[row.Week][row.SeriesId] = row.Value } weeks := make([]int, 0, len(weekSet)) for week := range weekSet { weeks = append(weeks, week) } sort.Ints(weeks) return weeks, values } func mapComparisonUniformityRows(rows []repository.ComparisonUniformityMetric) ([]int, map[int]map[uint]float64, []int, map[int]map[uint]float64) { bodyWeightSet := map[int]struct{}{} bodyWeightValues := map[int]map[uint]float64{} uniformitySet := map[int]struct{}{} uniformityValues := map[int]map[uint]float64{} for _, row := range rows { if row.Week <= 0 { continue } bodyWeightSet[row.Week] = struct{}{} uniformitySet[row.Week] = struct{}{} if bodyWeightValues[row.Week] == nil { bodyWeightValues[row.Week] = map[uint]float64{} } if uniformityValues[row.Week] == nil { uniformityValues[row.Week] = map[uint]float64{} } bodyWeightValues[row.Week][row.SeriesId] = row.AverageWeight uniformityValues[row.Week][row.SeriesId] = row.Uniformity } bodyWeightWeeks := make([]int, 0, len(bodyWeightSet)) for week := range bodyWeightSet { bodyWeightWeeks = append(bodyWeightWeeks, week) } sort.Ints(bodyWeightWeeks) uniformityWeeks := make([]int, 0, len(uniformitySet)) for week := range uniformitySet { uniformityWeeks = append(uniformityWeeks, week) } sort.Ints(uniformityWeeks) return bodyWeightWeeks, bodyWeightValues, uniformityWeeks, uniformityValues } func mergeComparisonWeeks(rows ...interface{}) []int { weekSet := map[int]struct{}{} for _, row := range rows { switch typed := row.(type) { case []repository.ComparisonWeeklyMetric: for _, item := range typed { if item.Week > 0 { weekSet[item.Week] = struct{}{} } } case []repository.ComparisonUniformityMetric: for _, item := range typed { if item.Week > 0 { weekSet[item.Week] = struct{}{} } } } } weeks := make([]int, 0, len(weekSet)) for week := range weekSet { weeks = append(weeks, week) } sort.Ints(weeks) return weeks } func buildComparisonPercentChart(seriesRows []repository.ComparisonSeries, weeks []int, actual map[int]map[uint]float64, standard map[int]float64) dto.DashboardChartDTO { series := make([]dto.DashboardChartSeriesDTO, 0, len(seriesRows)) for _, row := range seriesRows { series = append(series, dto.DashboardChartSeriesDTO{ Id: strconv.FormatUint(uint64(row.Id), 10), Label: row.Label, Unit: "%", }) } dataset := make([]map[string]interface{}, 0, len(weeks)) for _, week := range weeks { row := map[string]interface{}{ "week": week, } std := standard[week] for _, sRow := range seriesRows { key := strconv.FormatUint(uint64(sRow.Id), 10) actualVal := actual[week][sRow.Id] percent := 0.0 if std > 0 { percent = (actualVal / std) * 100 } row[key] = roundTo(percent, 2) } dataset = append(dataset, row) } return dto.DashboardChartDTO{ Series: series, Dataset: dataset, } } func percentDelta(current, last float64) float64 { if last <= 0 { return 0 } return (current - last) / last } func (s dashboardService) calculateHppGlobal(ctx context.Context, startDate, endExclusive, endDate time.Time, location *time.Location) (float64, float64, error) { if s.HppSvc != nil { currentHpp, err := s.hppGlobalForPeriod(ctx, startDate, endExclusive) if err != nil { return 0, 0, err } lastMonthStart, lastMonthEndExclusive := monthRange(endDate.AddDate(0, -1, 0), location) lastHpp, err := s.hppGlobalForPeriod(ctx, lastMonthStart, lastMonthEndExclusive) if err != nil { return 0, 0, err } return currentHpp, lastHpp, nil } totalEggKg, err := s.Repository.SumEggProductionWeightKg(ctx, startDate, endExclusive, nil) if err != nil { return 0, 0, err } totalCost, err := s.sumHppCost(ctx, nil, startDate, endExclusive) if err != nil { return 0, 0, err } hppCurrent := 0.0 if totalEggKg > 0 { hppCurrent = totalCost / totalEggKg } lastMonthStart, lastMonthEndExclusive := monthRange(endDate.AddDate(0, -1, 0), location) lastEggKg, err := s.Repository.SumEggProductionWeightKg(ctx, lastMonthStart, lastMonthEndExclusive, nil) if err != nil { return 0, 0, err } lastCost, err := s.sumHppCost(ctx, nil, lastMonthStart, lastMonthEndExclusive) if err != nil { return 0, 0, err } hppLast := 0.0 if lastEggKg > 0 { hppLast = lastCost / lastEggKg } return hppCurrent, hppLast, nil } func (s dashboardService) hppGlobalForPeriod(ctx context.Context, startDate, endExclusive time.Time) (float64, error) { kandangIDs, err := s.Repository.ListProjectFlockKandangIDsByEggProduction(ctx, startDate, endExclusive, nil) if err != nil { return 0, err } if len(kandangIDs) == 0 { return 0, nil } endOfPeriod := endExclusive.Add(-time.Nanosecond) totalCost := 0.0 totalWeightKg := 0.0 for _, kandangID := range kandangIDs { hppCost, err := s.HppSvc.CalculateHppCost(kandangID, &endOfPeriod) if err != nil { return 0, err } if hppCost == nil { continue } totalCost += hppCost.Estimation.Total totalWeightKg += hppCost.Estimation.Kg } if totalWeightKg <= 0 { return 0, nil } return totalCost / totalWeightKg, nil } func (s dashboardService) calculateSellingPrice(ctx context.Context, endDate time.Time, location *time.Location) (float64, float64, error) { startPrevMonth, endPrevMonthExclusive := monthRange(endDate.AddDate(0, -1, 0), location) currentEndExclusive := endDate.AddDate(0, 0, 1) currentAvg, err := s.avgSellingPrice(ctx, nil, startPrevMonth, currentEndExclusive) if err != nil { return 0, 0, err } lastAvg, err := s.avgSellingPrice(ctx, nil, startPrevMonth, endPrevMonthExclusive) if err != nil { return 0, 0, err } return currentAvg, lastAvg, nil } func (s dashboardService) calculateFcr(ctx context.Context, filter *validation.DashboardFilter, startDate, endExclusive, endDate time.Time, location *time.Location) (float64, float64, error) { current, err := s.fcrValue(ctx, filter, startDate, endExclusive) if err != nil { return 0, 0, err } lastMonthStart, lastMonthEndExclusive := monthRange(endDate.AddDate(0, -1, 0), location) last, err := s.fcrValue(ctx, filter, lastMonthStart, lastMonthEndExclusive) if err != nil { return 0, 0, err } return current, last, nil } func (s dashboardService) calculateMortality(ctx context.Context, filter *validation.DashboardFilter, startDate, endExclusive, endDate time.Time, location *time.Location) (float64, float64, error) { current, err := s.mortalityValue(ctx, filter, startDate, endExclusive) if err != nil { return 0, 0, err } lastMonthStart, lastMonthEndExclusive := monthRange(endDate.AddDate(0, -1, 0), location) last, err := s.mortalityValue(ctx, filter, lastMonthStart, lastMonthEndExclusive) if err != nil { return 0, 0, err } return current, last, nil } func (s dashboardService) fcrValue(ctx context.Context, filter *validation.DashboardFilter, startDate, endExclusive time.Time) (float64, error) { eggWeightGrams, err := s.Repository.SumEggProductionWeightGrams(ctx, startDate, endExclusive, filter) if err != nil { return 0, err } feedRows, err := s.Repository.GetFeedUsageByUom(ctx, startDate, endExclusive, filter) if err != nil { return 0, err } feedUsageGrams := feedUsageToGrams(feedRows) if eggWeightGrams <= 0 { return 0, nil } return feedUsageGrams / eggWeightGrams, nil } func (s dashboardService) mortalityValue(ctx context.Context, filter *validation.DashboardFilter, startDate, endExclusive time.Time) (float64, error) { depletions, err := s.Repository.SumDepletions(ctx, startDate, endExclusive, filter) if err != nil { return 0, err } initialPopulation, err := s.Repository.SumInitialPopulation(ctx, endExclusive.AddDate(0, 0, -1), filter) if err != nil { return 0, err } if initialPopulation <= 0 { return 0, nil } return (depletions / initialPopulation) * 100, nil } func (s dashboardService) sumHppCost(ctx context.Context, filter *validation.DashboardFilter, startDate, endExclusive time.Time) (float64, error) { sapronak, err := s.Repository.SumSapronakCost(ctx, startDate, endExclusive, filter) if err != nil { return 0, err } bop, err := s.Repository.SumBopCost(ctx, startDate, endExclusive, filter) if err != nil { return 0, err } ekspedisi, err := s.Repository.SumEkspedisiCost(ctx, startDate, endExclusive, filter) if err != nil { return 0, err } return sapronak + bop + ekspedisi, nil } func (s dashboardService) avgSellingPrice(ctx context.Context, filter *validation.DashboardFilter, startDate, endExclusive time.Time) (float64, error) { result, err := s.Repository.SumSellingPrice(ctx, startDate, endExclusive, filter) if err != nil { return 0, err } if result.TotalWeight <= 0 { return 0, nil } return result.TotalPrice / result.TotalWeight, nil } func extendBodyWeightDatasetUntilEndDate( dataset []map[string]interface{}, indexByWeek map[int]int, uniformities []repository.UniformityWeeklyMetric, uniformityMap map[int]repository.UniformityWeeklyMetric, standardMap map[int]repository.StandardWeeklyMetric, periodEnd time.Time, ) []map[string]interface{} { latestUniformityWeek := 0 var latestUniformityDate time.Time for _, row := range uniformities { if row.Week <= 0 || row.UniformDate.IsZero() { continue } if latestUniformityDate.IsZero() || row.UniformDate.After(latestUniformityDate) || (row.UniformDate.Equal(latestUniformityDate) && row.Week > latestUniformityWeek) { latestUniformityDate = row.UniformDate latestUniformityWeek = row.Week } } if latestUniformityWeek <= 0 || latestUniformityDate.IsZero() || periodEnd.IsZero() || !periodEnd.After(latestUniformityDate) { return dataset } additionalWeeks := int(math.Ceil(periodEnd.Sub(latestUniformityDate).Hours() / (24 * 7))) if additionalWeeks <= 0 { return dataset } lastUniformity := uniformityMap[latestUniformityWeek] lastStandard := standardMap[latestUniformityWeek] latestBodyWeight := roundTo(lastUniformity.AverageWeight, 2) latestStdBodyWeight := roundTo(lastStandard.StdBodyWeight, 2) targetWeek := latestUniformityWeek + additionalWeeks for week := latestUniformityWeek + 1; week <= targetWeek; week++ { row := map[string]interface{}{ "week": week, "body_weight": latestBodyWeight, "std_body_weight": latestStdBodyWeight, } if idx, ok := indexByWeek[week]; ok { dataset[idx] = row continue } dataset = append(dataset, row) indexByWeek[week] = len(dataset) - 1 } sort.Slice(dataset, func(i, j int) bool { return datasetWeek(dataset[i]) < datasetWeek(dataset[j]) }) return dataset } func datasetWeek(row map[string]interface{}) int { week, _ := row["week"].(int) return week } func feedUsageToGrams(rows []repository.FeedUsageByUom) float64 { total := 0.0 for _, row := range rows { total += feedUsageRowToGrams(row.TotalQty, row.UomName) } return total } func feedUsageRowToGrams(totalQty float64, uomName string) float64 { if totalQty <= 0 { return 0 } switch strings.TrimSpace(strings.ToLower(uomName)) { case "kilogram", "kg", "kilograms", "kilo": return totalQty * 1000 case "gram", "g", "grams": return totalQty default: return totalQty } } func roundTo(value float64, decimals int) float64 { if decimals <= 0 { return math.Round(value) } multiplier := math.Pow(10, float64(decimals)) return math.Round(value*multiplier) / multiplier } func monthRange(t time.Time, location *time.Location) (time.Time, time.Time) { start := time.Date(t.Year(), t.Month(), 1, 0, 0, 0, 0, location) endExclusive := start.AddDate(0, 1, 0) return start, endExclusive } func currentPeriodDates(location *time.Location) (time.Time, time.Time, time.Time) { now := time.Now().In(location) startDate := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, location) endDate := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, location) endExclusive := endDate.AddDate(0, 0, 1) return startDate, endDate, endExclusive }