package service import ( "context" "errors" "fmt" "math" "sort" "strconv" "strings" "time" "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" ) type DashboardService interface { GetAll(ctx context.Context, params *validation.Query) (dto.DashboardPerformanceOverviewDTO, int64, error) } type dashboardService struct { Log *logrus.Logger Validate *validator.Validate Repository repository.DashboardRepository } func NewDashboardService(repo repository.DashboardRepository, validate *validator.Validate) DashboardService { return &dashboardService{ Log: utils.Log, Validate: validate, Repository: repo, } } 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 hppCurrent, hppLast, err := s.calculateHppGlobal(ctx, filter, startDate, endExclusive, endDate, location) if err != nil { return nil, err } sellingCurrent, sellingLast, err := s.calculateSellingPrice(ctx, filter, endDate, 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 := 0.0 if hppLast > 0 { hppPercent = (hppCurrent - hppLast) / hppLast * 100 } sellingPercent := 0.0 if sellingLast > 0 { sellingPercent = sellingCurrent / sellingLast * 100 } stats := []dto.DashboardStatisticsDTO{ { Label: "HPP Global", Value: roundTo(hppCurrent, 0), PercentLastMonth: hppPercent, }, { Label: "Avg. Selling Price", Value: roundTo(sellingCurrent, 0), PercentLastMonth: sellingPercent, }, } if hasFilter { fcrPercent := 0.0 if fcrLast > 0 { fcrPercent = (fcrCurrent - fcrLast) / fcrLast * 100 } mortalityPercent := 0.0 if mortalityLast > 0 { mortalityPercent = (mortalityCurrent - mortalityLast) / mortalityLast * 100 } stats = append(stats, dto.DashboardStatisticsDTO{ Label: "FCR", Value: roundTo(fcrCurrent, 2), PercentLastMonth: fcrPercent, }, dto.DashboardStatisticsDTO{ Label: "Mortality", Value: roundTo(mortalityCurrent, 2), PercentLastMonth: mortalityPercent, }, ) } 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)) 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 := recordingMap[week] uni := uniformityMap[week] std := standardMap[week] stdFcr := standardFcrMap[week] weekEgg := weeklyEggMap[week] weekFeed := weeklyFeedMap[week] actFcr := 0.0 if weekFeed > 0 { actFcr = weekEgg / weekFeed } cumEgg += weekEgg cumFeed += weekFeed actFcrCum := 0.0 if cumFeed > 0 { actFcrCum = cumEgg / cumFeed } bodyWeightDataset = append(bodyWeightDataset, map[string]interface{}{ "week": week, "body_weight": roundTo(uni.AverageWeight, 2), "std_body_weight": roundTo(std.StdBodyWeight, 2), }) performanceDataset = append(performanceDataset, map[string]interface{}{ "week": week, "act_laying": roundTo(rec.HenDay, 2), "std_laying": roundTo(std.StdLaying, 2), "act_egg_weight": roundTo(rec.EggWeight, 2), "std_egg_weight": roundTo(std.StdEggWeight, 2), "act_feed_intake": roundTo(rec.FeedIntake, 2), "std_feed_intake": roundTo(std.StdFeedIntake, 2), "act_uniformity": roundTo(uni.Uniformity, 2), "std_uniformity": roundTo(std.StdUniformity, 2), }) fcrDataset = append(fcrDataset, map[string]interface{}{ "week": week, "act_fcr": roundTo(actFcr, 2), "std_fcr": roundTo(stdFcr, 2), "act_fcr_cum": roundTo(actFcrCum, 2), "std_fcr_cum": roundTo(stdFcr, 2), }) deplesiDataset = append(deplesiDataset, map[string]interface{}{ "week": week, "act_deplesi": roundTo(rec.CumDepletionRate, 2), "std_deplesi": roundTo(std.StdDepletion, 2), }) } 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: "%"}, {Id: "std_fcr", Label: "STD. FCR", Unit: "%"}, {Id: "act_fcr_cum", Label: "Act. FCR Cummulative", Unit: "%"}, {Id: "std_fcr_cum", Label: "STD. FCR Cummulative", Unit: "%"}, }, 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 count == 0 { continue } if result[week] == nil { result[week] = map[uint]float64{} } 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 (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 (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 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 (s dashboardService) calculateHppGlobal(ctx context.Context, filter *validation.DashboardFilter, startDate, endExclusive, endDate time.Time, location *time.Location) (float64, float64, error) { totalEggKg, err := s.Repository.SumEggProductionWeightKg(ctx, startDate, endExclusive, filter) if err != nil { return 0, 0, err } totalCost, err := s.sumHppCost(ctx, filter, 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, filter) if err != nil { return 0, 0, err } lastCost, err := s.sumHppCost(ctx, filter, 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) calculateSellingPrice(ctx context.Context, filter *validation.DashboardFilter, 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, filter, startPrevMonth, currentEndExclusive) if err != nil { return 0, 0, err } lastAvg, err := s.avgSellingPrice(ctx, filter, 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 feedUsageGrams <= 0 { return 0, nil } return eggWeightGrams / feedUsageGrams, 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 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 }