Files
lti-api/internal/modules/dashboards/services/dashboard.service.go
T
2026-01-14 13:30:48 +07:00

1040 lines
30 KiB
Go

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
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))
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 weekEgg > 0 {
actFcr = weekFeed / weekEgg
}
cumEgg += weekEgg
cumFeed += weekFeed
actFcrCum := 0.0
if cumEgg > 0 {
actFcrCum = cumFeed / cumEgg
}
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: "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 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 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) {
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) 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 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
}