mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
1030 lines
30 KiB
Go
1030 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
|
|
|
|
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 := 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 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 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, 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
|
|
}
|