mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-22 22:35:43 +00:00
feat(BE-390): calculation dashboard
This commit is contained in:
@@ -0,0 +1,867 @@
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
fcrPercent := 0.0
|
||||
if fcrLast > 0 {
|
||||
fcrPercent = (fcrCurrent - fcrLast) / fcrLast * 100
|
||||
}
|
||||
|
||||
mortalityPercent := 0.0
|
||||
if mortalityLast > 0 {
|
||||
mortalityPercent = (mortalityCurrent - mortalityLast) / mortalityLast * 100
|
||||
}
|
||||
|
||||
return []dto.DashboardStatisticsDTO{
|
||||
{
|
||||
Label: "HPP Global",
|
||||
Value: roundTo(hppCurrent, 0),
|
||||
PercentLastMonth: hppPercent,
|
||||
},
|
||||
{
|
||||
Label: "Avg. Selling Price",
|
||||
Value: roundTo(sellingCurrent, 0),
|
||||
PercentLastMonth: sellingPercent,
|
||||
},
|
||||
{
|
||||
Label: "FCR",
|
||||
Value: roundTo(fcrCurrent, 2),
|
||||
PercentLastMonth: fcrPercent,
|
||||
},
|
||||
{
|
||||
Label: "Mortality",
|
||||
Value: roundTo(mortalityCurrent, 2),
|
||||
PercentLastMonth: mortalityPercent,
|
||||
},
|
||||
}, 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")
|
||||
}
|
||||
|
||||
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.HandDay, 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
|
||||
}
|
||||
|
||||
layingWeeks, layingActual := mapComparisonWeeklyMetricRows(layingRows)
|
||||
eggWeightWeeks, eggWeightActual := mapComparisonWeeklyMetricRows(eggWeightRows)
|
||||
feedWeeks, feedActual := mapComparisonWeeklyMetricRows(feedIntakeRows)
|
||||
fcrWeeks, fcrActual := mapComparisonWeeklyMetricRows(fcrRows)
|
||||
deplesiWeeks, deplesiActual := mapComparisonWeeklyMetricRows(deplesiRows)
|
||||
bodyWeightWeeks, bodyWeightActual, uniformityWeeks, uniformityActual := mapComparisonUniformityRows(uniformityRows)
|
||||
|
||||
charts := map[string]dto.DashboardChartDTO{}
|
||||
if len(bodyWeightWeeks) > 0 {
|
||||
charts["body_weight"] = buildComparisonPercentChart(seriesRows, bodyWeightWeeks, bodyWeightActual, stdBodyWeight)
|
||||
}
|
||||
if len(layingWeeks) > 0 {
|
||||
charts["laying"] = buildComparisonPercentChart(seriesRows, layingWeeks, layingActual, stdLaying)
|
||||
}
|
||||
if len(eggWeightWeeks) > 0 {
|
||||
charts["egg_weight"] = buildComparisonPercentChart(seriesRows, eggWeightWeeks, eggWeightActual, stdEggWeight)
|
||||
}
|
||||
if len(feedWeeks) > 0 {
|
||||
charts["feed_intake"] = buildComparisonPercentChart(seriesRows, feedWeeks, feedActual, stdFeedIntake)
|
||||
}
|
||||
if len(uniformityWeeks) > 0 {
|
||||
charts["uniformity"] = buildComparisonPercentChart(seriesRows, uniformityWeeks, uniformityActual, stdUniformity)
|
||||
}
|
||||
if len(fcrWeeks) > 0 {
|
||||
charts["fcr"] = buildComparisonPercentChart(seriesRows, fcrWeeks, fcrActual, stdFcr)
|
||||
}
|
||||
if len(deplesiWeeks) > 0 {
|
||||
charts["deplesi"] = buildComparisonPercentChart(seriesRows, deplesiWeeks, deplesiActual, stdDeplesi)
|
||||
}
|
||||
|
||||
return charts, nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user