mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 05:21:57 +00:00
feat(BE-390): calculation dashboard
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Dashboard struct {
|
||||
Id uint `gorm:"primaryKey"`
|
||||
Name string `gorm:"not null;uniqueIndex:idx_name,where:deleted_at IS NULL"`
|
||||
CreatedBy uint `gorm:"not null"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
|
||||
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/modules/dashboards/dto"
|
||||
service "gitlab.com/mbugroup/lti-api.git/internal/modules/dashboards/services"
|
||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/dashboards/validations"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/response"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
type DashboardController struct {
|
||||
DashboardService service.DashboardService
|
||||
}
|
||||
|
||||
func NewDashboardController(dashboardService service.DashboardService) *DashboardController {
|
||||
return &DashboardController{
|
||||
DashboardService: dashboardService,
|
||||
}
|
||||
}
|
||||
|
||||
func (u *DashboardController) GetAll(c *fiber.Ctx) error {
|
||||
parseStringListParam := func(param string) ([]string, error) {
|
||||
if param == "" {
|
||||
return nil, nil
|
||||
}
|
||||
parts := strings.Split(param, ",")
|
||||
result := make([]string, 0, len(parts))
|
||||
for _, part := range parts {
|
||||
trimmed := strings.TrimSpace(part)
|
||||
if trimmed == "" {
|
||||
return nil, strconv.ErrSyntax
|
||||
}
|
||||
result = append(result, trimmed)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
parseUintListParam := func(param string) ([]uint, error) {
|
||||
if param == "" {
|
||||
return nil, nil
|
||||
}
|
||||
parts := strings.Split(param, ",")
|
||||
ids := make([]uint, 0, len(parts))
|
||||
for _, part := range parts {
|
||||
trimmed := strings.TrimSpace(part)
|
||||
if trimmed == "" {
|
||||
return nil, strconv.ErrSyntax
|
||||
}
|
||||
parsed, err := strconv.ParseUint(trimmed, 10, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ids = append(ids, uint(parsed))
|
||||
}
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
lokasiIds, err := parseUintListParam(c.Query("lokasi_ids", ""))
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid lokasi_ids")
|
||||
}
|
||||
|
||||
flockIds, err := parseUintListParam(c.Query("flock_ids", ""))
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid flock_ids")
|
||||
}
|
||||
|
||||
kandangIds, err := parseUintListParam(c.Query("kandang_ids", ""))
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid kandang_ids")
|
||||
}
|
||||
|
||||
include, err := parseStringListParam(strings.ToLower(c.Query("include", "")))
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid include")
|
||||
}
|
||||
|
||||
analysisMode := strings.ToUpper(strings.TrimSpace(c.Query("analysis_mode", validation.AnalysisModeOverview)))
|
||||
metric := strings.ToLower(strings.TrimSpace(c.Query("metric", "")))
|
||||
|
||||
query := &validation.Query{
|
||||
Page: c.QueryInt("page", 1),
|
||||
Limit: c.QueryInt("limit", 10),
|
||||
Search: strings.TrimSpace(c.Query("search", "")),
|
||||
PerformanceOverviewFilter: validation.PerformanceOverviewFilter{
|
||||
StartDate: c.Query("start_date", ""),
|
||||
EndDate: c.Query("end_date", ""),
|
||||
AnalysisMode: analysisMode,
|
||||
ComparisonType: strings.ToUpper(strings.TrimSpace(c.Query("comparison_type", ""))),
|
||||
Metric: metric,
|
||||
LokasiIds: lokasiIds,
|
||||
FlockIds: flockIds,
|
||||
KandangIds: kandangIds,
|
||||
Include: include,
|
||||
},
|
||||
}
|
||||
|
||||
if query.Page < 1 || query.Limit < 1 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
|
||||
}
|
||||
|
||||
if query.AnalysisMode == validation.AnalysisModeComparison && query.ComparisonType == "" {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "comparison_type is required for comparison mode")
|
||||
}
|
||||
|
||||
location, err := time.LoadLocation("Asia/Jakarta")
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "failed to load timezone configuration")
|
||||
}
|
||||
|
||||
startDate, endDate, endExclusive, err := parsePeriodDates(query.StartDate, query.EndDate, location)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
query.PeriodStart = startDate
|
||||
query.PeriodEnd = endDate
|
||||
query.PeriodEndExclusive = endExclusive
|
||||
|
||||
result, totalResults, err := u.DashboardService.GetAll(c.Context(), query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
filters := dto.DashboardFiltersDTO{
|
||||
StartDate: query.StartDate,
|
||||
EndDate: query.EndDate,
|
||||
AnalysisMode: query.AnalysisMode,
|
||||
ComparisonType: query.ComparisonType,
|
||||
Metric: query.Metric,
|
||||
LokasiIds: defaultUintSlice(query.LokasiIds),
|
||||
FlockIds: defaultUintSlice(query.FlockIds),
|
||||
KandangIds: defaultUintSlice(query.KandangIds),
|
||||
Include: query.Include,
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).
|
||||
JSON(response.SuccessWithMeta{
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: "Get dashboard successfully",
|
||||
Meta: response.Meta{
|
||||
Page: query.Page,
|
||||
Limit: query.Limit,
|
||||
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
|
||||
TotalResults: totalResults,
|
||||
Filters: filters,
|
||||
},
|
||||
Data: result,
|
||||
})
|
||||
}
|
||||
|
||||
func defaultUintSlice(values []uint) []uint {
|
||||
if values == nil {
|
||||
return []uint{}
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
func parsePeriodDates(startDateRaw, endDateRaw string, location *time.Location) (time.Time, time.Time, time.Time, error) {
|
||||
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)
|
||||
|
||||
if startDateRaw != "" {
|
||||
parsed, err := time.ParseInLocation("2006-01-02", startDateRaw, location)
|
||||
if err != nil {
|
||||
return time.Time{}, time.Time{}, time.Time{}, fiber.NewError(fiber.StatusBadRequest, "start_date must follow format YYYY-MM-DD")
|
||||
}
|
||||
startDate = parsed
|
||||
}
|
||||
|
||||
if endDateRaw != "" {
|
||||
parsed, err := time.ParseInLocation("2006-01-02", endDateRaw, location)
|
||||
if err != nil {
|
||||
return time.Time{}, time.Time{}, time.Time{}, fiber.NewError(fiber.StatusBadRequest, "end_date must follow format YYYY-MM-DD")
|
||||
}
|
||||
endDate = parsed
|
||||
}
|
||||
|
||||
if endDate.Before(startDate) {
|
||||
return time.Time{}, time.Time{}, time.Time{}, fiber.NewError(fiber.StatusBadRequest, "end_date must be greater than or equal to start_date")
|
||||
}
|
||||
|
||||
endExclusive := endDate.AddDate(0, 0, 1)
|
||||
return startDate, endDate, endExclusive, nil
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
|
||||
)
|
||||
|
||||
// === DTO Structs ===
|
||||
|
||||
type DashboardListDTO struct {
|
||||
Id uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
CreatedUser *userDTO.UserRelationDTO `json:"created_user"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type DashboardDetailDTO struct {
|
||||
DashboardListDTO
|
||||
}
|
||||
|
||||
type DashboardFiltersDTO struct {
|
||||
StartDate string `json:"start_date"`
|
||||
EndDate string `json:"end_date"`
|
||||
AnalysisMode string `json:"analysis_mode"`
|
||||
ComparisonType string `json:"comparison_type,omitempty"`
|
||||
Metric string `json:"metric,omitempty"`
|
||||
LokasiIds []uint `json:"lokasi_ids"`
|
||||
FlockIds []uint `json:"flock_ids"`
|
||||
KandangIds []uint `json:"kandang_ids"`
|
||||
Include []string `json:"include,omitempty"`
|
||||
}
|
||||
|
||||
type DashboardStatisticsDTO struct {
|
||||
Label string `json:"label"`
|
||||
Value float64 `json:"value"`
|
||||
PercentLastMonth float64 `json:"percent_last_month"`
|
||||
}
|
||||
|
||||
type DashboardPerformanceOverviewDTO struct {
|
||||
StatisticsData []DashboardStatisticsDTO `json:"statistics_data"`
|
||||
Charts map[string]DashboardChartDTO `json:"charts,omitempty"`
|
||||
}
|
||||
|
||||
type DashboardChartSeriesDTO struct {
|
||||
Id string `json:"id"`
|
||||
Label string `json:"label"`
|
||||
Unit string `json:"unit,omitempty"`
|
||||
}
|
||||
|
||||
type DashboardChartDTO struct {
|
||||
Series []DashboardChartSeriesDTO `json:"series"`
|
||||
Dataset []map[string]interface{} `json:"dataset"`
|
||||
}
|
||||
|
||||
// === Mapper Functions ===
|
||||
|
||||
func ToDashboardListDTO(e entity.Dashboard) DashboardListDTO {
|
||||
var createdUser *userDTO.UserRelationDTO
|
||||
if e.CreatedUser.Id != 0 {
|
||||
mapped := userDTO.ToUserRelationDTO(e.CreatedUser)
|
||||
createdUser = &mapped
|
||||
}
|
||||
|
||||
return DashboardListDTO{
|
||||
Id: e.Id,
|
||||
Name: e.Name,
|
||||
CreatedAt: e.CreatedAt,
|
||||
UpdatedAt: e.UpdatedAt,
|
||||
CreatedUser: createdUser,
|
||||
}
|
||||
}
|
||||
|
||||
func ToDashboardListDTOs(e []entity.Dashboard) []DashboardListDTO {
|
||||
result := make([]DashboardListDTO, len(e))
|
||||
for i, r := range e {
|
||||
result[i] = ToDashboardListDTO(r)
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package dashboards
|
||||
|
||||
import (
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"gorm.io/gorm"
|
||||
|
||||
rDashboard "gitlab.com/mbugroup/lti-api.git/internal/modules/dashboards/repositories"
|
||||
sDashboard "gitlab.com/mbugroup/lti-api.git/internal/modules/dashboards/services"
|
||||
|
||||
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
||||
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||
)
|
||||
|
||||
type DashboardModule struct{}
|
||||
|
||||
func (DashboardModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
||||
dashboardRepo := rDashboard.NewDashboardRepository(db)
|
||||
userRepo := rUser.NewUserRepository(db)
|
||||
|
||||
dashboardService := sDashboard.NewDashboardService(dashboardRepo, validate)
|
||||
userService := sUser.NewUserService(userRepo, validate)
|
||||
|
||||
DashboardRoutes(router, userService, dashboardService)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/dashboards/validations"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type DashboardRepository interface {
|
||||
repository.BaseRepository[entity.Dashboard]
|
||||
GetFeedUsageByUom(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) ([]FeedUsageByUom, error)
|
||||
SumDepletions(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) (float64, error)
|
||||
SumInitialPopulation(ctx context.Context, endDate time.Time, filters *validation.DashboardFilter) (float64, error)
|
||||
SumSapronakCost(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) (float64, error)
|
||||
SumBopCost(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) (float64, error)
|
||||
SumEkspedisiCost(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) (float64, error)
|
||||
SumSellingPrice(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) (SellingPriceAggregate, error)
|
||||
SumEggProductionWeightGrams(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) (float64, error)
|
||||
SumEggProductionWeightKg(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) (float64, error)
|
||||
GetRecordingWeeklyMetrics(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) ([]RecordingWeeklyMetric, error)
|
||||
GetUniformityWeeklyMetrics(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) ([]UniformityWeeklyMetric, error)
|
||||
GetStandardWeeklyMetrics(ctx context.Context, weeks []int, filters *validation.DashboardFilter) ([]StandardWeeklyMetric, error)
|
||||
GetStandardFcrWeekly(ctx context.Context, weeks []int, filters *validation.DashboardFilter) ([]StandardWeeklyFcrMetric, error)
|
||||
GetComparisonSeries(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter, comparisonType string) ([]ComparisonSeries, error)
|
||||
GetComparisonWeeklyMetrics(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter, comparisonType, metric string) ([]ComparisonWeeklyMetric, error)
|
||||
GetComparisonWeeklyUniformityMetrics(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter, comparisonType string) ([]ComparisonUniformityMetric, error)
|
||||
GetEggQualityWeeklyMetrics(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) ([]EggQualityWeeklyMetric, error)
|
||||
GetEggWeightWeeklyGrams(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) ([]WeeklyEggWeightMetric, error)
|
||||
GetFeedUsageWeeklyByUom(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) ([]WeeklyFeedUsageMetric, error)
|
||||
}
|
||||
|
||||
type DashboardRepositoryImpl struct {
|
||||
*repository.BaseRepositoryImpl[entity.Dashboard]
|
||||
}
|
||||
|
||||
func NewDashboardRepository(db *gorm.DB) DashboardRepository {
|
||||
return &DashboardRepositoryImpl{
|
||||
BaseRepositoryImpl: repository.NewBaseRepository[entity.Dashboard](db),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,672 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/dashboards/validations"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type SellingPriceAggregate struct {
|
||||
TotalPrice float64
|
||||
TotalWeight float64
|
||||
}
|
||||
|
||||
type FeedUsageByUom struct {
|
||||
TotalQty float64
|
||||
UomName string
|
||||
}
|
||||
|
||||
type RecordingWeeklyMetric struct {
|
||||
Week int
|
||||
HandDay float64
|
||||
EggWeight float64
|
||||
FeedIntake float64
|
||||
FcrValue float64
|
||||
CumDepletionRate float64
|
||||
}
|
||||
|
||||
type UniformityWeeklyMetric struct {
|
||||
Week int
|
||||
Uniformity float64
|
||||
AverageWeight float64
|
||||
}
|
||||
|
||||
type StandardWeeklyMetric struct {
|
||||
Week int
|
||||
StdLaying float64
|
||||
StdEggWeight float64
|
||||
StdFeedIntake float64
|
||||
StdUniformity float64
|
||||
StdDepletion float64
|
||||
StdBodyWeight float64
|
||||
}
|
||||
|
||||
type StandardWeeklyFcrMetric struct {
|
||||
Week int
|
||||
StdFcr float64
|
||||
}
|
||||
|
||||
type ComparisonSeries struct {
|
||||
Id uint
|
||||
Label string
|
||||
}
|
||||
|
||||
type ComparisonWeeklyMetric struct {
|
||||
Week int
|
||||
SeriesId uint
|
||||
Value float64
|
||||
}
|
||||
|
||||
type ComparisonUniformityMetric struct {
|
||||
Week int
|
||||
SeriesId uint
|
||||
Uniformity float64
|
||||
AverageWeight float64
|
||||
}
|
||||
|
||||
type EggQualityWeeklyMetric struct {
|
||||
Week int
|
||||
NormalQty float64
|
||||
AbnormalQty float64
|
||||
TotalQty float64
|
||||
}
|
||||
|
||||
type WeeklyEggWeightMetric struct {
|
||||
Week int
|
||||
EggWeightGrams float64
|
||||
}
|
||||
|
||||
type WeeklyFeedUsageMetric struct {
|
||||
Week int
|
||||
TotalQty float64
|
||||
UomName string
|
||||
}
|
||||
|
||||
func applyDashboardFilters(db *gorm.DB, filters *validation.DashboardFilter) *gorm.DB {
|
||||
if filters == nil {
|
||||
return db
|
||||
}
|
||||
if len(filters.FlockIds) > 0 {
|
||||
db = db.Where("pfk.project_flock_id IN ?", filters.FlockIds)
|
||||
}
|
||||
if len(filters.KandangIds) > 0 {
|
||||
db = db.Where("k.id IN ?", filters.KandangIds)
|
||||
}
|
||||
if len(filters.LokasiIds) > 0 {
|
||||
db = db.Where("k.location_id IN ?", filters.LokasiIds)
|
||||
}
|
||||
return db
|
||||
}
|
||||
|
||||
func (r *DashboardRepositoryImpl) GetRecordingWeeklyMetrics(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) ([]RecordingWeeklyMetric, error) {
|
||||
var rows []RecordingWeeklyMetric
|
||||
|
||||
db := r.DB().WithContext(ctx).
|
||||
Table("recordings AS r").
|
||||
Select(`((r.day - 1) / 7 + 1) AS week,
|
||||
COALESCE(AVG(r.hen_day), 0) AS hand_day,
|
||||
COALESCE(AVG(r.egg_weight), 0) AS egg_weight,
|
||||
COALESCE(AVG(r.feed_intake), 0) AS feed_intake,
|
||||
COALESCE(AVG(r.fcr_value), 0) AS fcr_value,
|
||||
COALESCE(AVG(r.cum_depletion_rate), 0) AS cum_depletion_rate`).
|
||||
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id").
|
||||
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
|
||||
Where("r.record_datetime >= ? AND r.record_datetime < ?", start, end).
|
||||
Where("r.deleted_at IS NULL").
|
||||
Where("r.day IS NOT NULL AND r.day > 0")
|
||||
|
||||
db = applyDashboardFilters(db, filters)
|
||||
|
||||
if err := db.Group("week").Order("week ASC").Scan(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
func (r *DashboardRepositoryImpl) GetUniformityWeeklyMetrics(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) ([]UniformityWeeklyMetric, error) {
|
||||
var rows []UniformityWeeklyMetric
|
||||
|
||||
db := r.DB().WithContext(ctx).
|
||||
Table("project_flock_kandang_uniformity AS u").
|
||||
Select(`u.week AS week,
|
||||
COALESCE(AVG(u.uniformity), 0) AS uniformity,
|
||||
COALESCE(AVG((u.chart_data->'statistics'->>'average_weight')::numeric), 0) AS average_weight`).
|
||||
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = u.project_flock_kandang_id").
|
||||
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
|
||||
Where("u.uniform_date IS NOT NULL").
|
||||
Where("u.uniform_date >= ? AND u.uniform_date < ?", start, end)
|
||||
|
||||
db = applyDashboardFilters(db, filters)
|
||||
|
||||
if err := db.Group("u.week").Order("u.week ASC").Scan(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
func (r *DashboardRepositoryImpl) GetStandardWeeklyMetrics(ctx context.Context, weeks []int, filters *validation.DashboardFilter) ([]StandardWeeklyMetric, error) {
|
||||
if len(weeks) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
standardIDs := r.standardIDSubquery(filters)
|
||||
if standardIDs == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var rows []StandardWeeklyMetric
|
||||
db := r.DB().WithContext(ctx).
|
||||
Table("standard_growth_details AS sgd").
|
||||
Select(`sgd.week AS week,
|
||||
COALESCE(AVG(psd.target_hen_day_production), 0) AS std_laying,
|
||||
COALESCE(AVG(psd.target_egg_weight), 0) AS std_egg_weight,
|
||||
COALESCE(AVG(sgd.feed_intake), 0) AS std_feed_intake,
|
||||
COALESCE(AVG(sgd.min_uniformity), 0) AS std_uniformity,
|
||||
COALESCE(AVG(sgd.max_depletion), 0) AS std_depletion,
|
||||
COALESCE(AVG(sgd.target_mean_bw), 0) AS std_body_weight`).
|
||||
Joins("LEFT JOIN production_standard_details AS psd ON psd.production_standard_id = sgd.production_standard_id AND psd.week = sgd.week").
|
||||
Where("sgd.week IN ?", weeks).
|
||||
Where("sgd.production_standard_id IN (?)", standardIDs)
|
||||
|
||||
if err := db.Group("sgd.week").Order("sgd.week ASC").Scan(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
func (r *DashboardRepositoryImpl) GetStandardFcrWeekly(ctx context.Context, weeks []int, filters *validation.DashboardFilter) ([]StandardWeeklyFcrMetric, error) {
|
||||
if len(weeks) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
source := r.standardSourceSubquery(filters)
|
||||
if source == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var rows []StandardWeeklyFcrMetric
|
||||
db := r.DB().WithContext(ctx).
|
||||
Table("standard_growth_details AS sgd").
|
||||
Select(`
|
||||
sgd.week AS week,
|
||||
COALESCE(AVG(
|
||||
COALESCE(
|
||||
(
|
||||
SELECT fs.fcr_number
|
||||
FROM fcr_standards fs
|
||||
WHERE fs.fcr_id = src.fcr_id
|
||||
AND fs.weight >= CASE WHEN sgd.target_mean_bw > 10 THEN sgd.target_mean_bw / 1000 ELSE sgd.target_mean_bw END
|
||||
ORDER BY fs.weight ASC
|
||||
LIMIT 1
|
||||
),
|
||||
(
|
||||
SELECT fs.fcr_number
|
||||
FROM fcr_standards fs
|
||||
WHERE fs.fcr_id = src.fcr_id
|
||||
ORDER BY fs.weight DESC
|
||||
LIMIT 1
|
||||
)
|
||||
)
|
||||
), 0) AS std_fcr`).
|
||||
Joins("JOIN (?) AS src ON src.production_standard_id = sgd.production_standard_id", source).
|
||||
Where("sgd.week IN ?", weeks)
|
||||
|
||||
if err := db.Group("sgd.week").Order("sgd.week ASC").Scan(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
func (r *DashboardRepositoryImpl) SumEggProductionWeightGrams(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) (float64, error) {
|
||||
var total float64
|
||||
|
||||
db := r.DB().WithContext(ctx).
|
||||
Table("recording_eggs AS re").
|
||||
Select("COALESCE(SUM(re.qty * re.weight), 0)").
|
||||
Joins("JOIN recordings AS r ON r.id = re.recording_id").
|
||||
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id").
|
||||
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
|
||||
Where("r.record_datetime >= ? AND r.record_datetime < ?", start, end).
|
||||
Where("r.deleted_at IS NULL")
|
||||
|
||||
db = applyDashboardFilters(db, filters)
|
||||
|
||||
if err := db.Scan(&total).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return total, nil
|
||||
}
|
||||
|
||||
func (r *DashboardRepositoryImpl) SumEggProductionWeightKg(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) (float64, error) {
|
||||
grams, err := r.SumEggProductionWeightGrams(ctx, start, end, filters)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return grams / 1000, nil
|
||||
}
|
||||
|
||||
func (r *DashboardRepositoryImpl) GetFeedUsageByUom(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) ([]FeedUsageByUom, error) {
|
||||
var rows []FeedUsageByUom
|
||||
|
||||
db := r.DB().WithContext(ctx).
|
||||
Table("recording_stocks AS rs").
|
||||
Select("COALESCE(SUM(rs.usage_qty), 0) + COALESCE(SUM(rs.pending_qty), 0) AS total_qty, LOWER(uoms.name) AS uom_name").
|
||||
Joins("JOIN recordings AS r ON r.id = rs.recording_id").
|
||||
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id").
|
||||
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
|
||||
Joins("JOIN product_warehouses AS pw ON pw.id = rs.product_warehouse_id").
|
||||
Joins("JOIN products AS p ON p.id = pw.product_id").
|
||||
Joins("JOIN uoms ON uoms.id = p.uom_id").
|
||||
Joins("JOIN flags AS f ON f.flagable_id = p.id AND f.flagable_type = ? AND UPPER(f.name) = ?", entity.FlagableTypeProduct, "PAKAN").
|
||||
Where("r.record_datetime >= ? AND r.record_datetime < ?", start, end).
|
||||
Where("r.deleted_at IS NULL")
|
||||
|
||||
db = applyDashboardFilters(db, filters)
|
||||
|
||||
if err := db.Group("LOWER(uoms.name)").Scan(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
func (r *DashboardRepositoryImpl) SumDepletions(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) (float64, error) {
|
||||
var total float64
|
||||
|
||||
db := r.DB().WithContext(ctx).
|
||||
Table("recording_depletions AS rd").
|
||||
Select("COALESCE(SUM(rd.qty), 0)").
|
||||
Joins("JOIN recordings AS r ON r.id = rd.recording_id").
|
||||
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id").
|
||||
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
|
||||
Where("r.record_datetime >= ? AND r.record_datetime < ?", start, end).
|
||||
Where("r.deleted_at IS NULL")
|
||||
|
||||
db = applyDashboardFilters(db, filters)
|
||||
|
||||
if err := db.Scan(&total).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return total, nil
|
||||
}
|
||||
|
||||
func (r *DashboardRepositoryImpl) SumInitialPopulation(ctx context.Context, endDate time.Time, filters *validation.DashboardFilter) (float64, error) {
|
||||
var total float64
|
||||
endOfDate := endDate.AddDate(0, 0, 1)
|
||||
|
||||
db := r.DB().WithContext(ctx).
|
||||
Table("project_chickins AS pc").
|
||||
Select("COALESCE(SUM(pc.usage_qty), 0)").
|
||||
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = pc.project_flock_kandang_id").
|
||||
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
|
||||
Where("pc.chick_in_date < ?", endOfDate).
|
||||
Where("pc.deleted_at IS NULL")
|
||||
|
||||
db = applyDashboardFilters(db, filters)
|
||||
|
||||
if err := db.Scan(&total).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return total, nil
|
||||
}
|
||||
|
||||
func (r *DashboardRepositoryImpl) SumSellingPrice(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) (SellingPriceAggregate, error) {
|
||||
var result SellingPriceAggregate
|
||||
|
||||
db := r.DB().WithContext(ctx).
|
||||
Table("marketing_delivery_products AS mdp").
|
||||
Select("COALESCE(SUM(mdp.total_price), 0) AS total_price, COALESCE(SUM(mdp.total_weight), 0) AS total_weight").
|
||||
Joins("JOIN marketing_products AS mp ON mp.id = mdp.marketing_product_id").
|
||||
Joins("JOIN product_warehouses AS pw ON pw.id = mp.product_warehouse_id").
|
||||
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = pw.project_flock_kandang_id").
|
||||
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
|
||||
Where("mdp.delivery_date IS NOT NULL").
|
||||
Where("mdp.delivery_date >= ? AND mdp.delivery_date < ?", start, end)
|
||||
|
||||
db = applyDashboardFilters(db, filters)
|
||||
|
||||
if err := db.Scan(&result).Error; err != nil {
|
||||
return SellingPriceAggregate{}, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *DashboardRepositoryImpl) SumSapronakCost(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) (float64, error) {
|
||||
var total float64
|
||||
|
||||
db := r.DB().WithContext(ctx).
|
||||
Table("purchase_items AS pi").
|
||||
Select("COALESCE(SUM(pi.total_price), 0) AS total").
|
||||
Joins("JOIN products AS p ON p.id = pi.product_id").
|
||||
Joins("JOIN flags AS f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct).
|
||||
Joins("LEFT JOIN product_warehouses AS pw ON pw.id = pi.product_warehouse_id").
|
||||
Joins("LEFT JOIN project_flock_kandangs AS pfk ON pfk.id = COALESCE(pi.project_flock_kandang_id, pw.project_flock_kandang_id)").
|
||||
Joins("LEFT JOIN kandangs AS k ON k.id = pfk.kandang_id").
|
||||
Where("f.name IN ?", []utils.FlagType{utils.FlagDOC, utils.FlagPakan, utils.FlagOVK}).
|
||||
Where("pi.received_date IS NOT NULL").
|
||||
Where("pi.received_date >= ? AND pi.received_date < ?", start, end)
|
||||
|
||||
db = applyDashboardFilters(db, filters)
|
||||
|
||||
if err := db.Scan(&total).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return total, nil
|
||||
}
|
||||
|
||||
func (r *DashboardRepositoryImpl) SumBopCost(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) (float64, error) {
|
||||
return r.sumExpenseRealization(ctx, start, end, filters, func(db *gorm.DB) *gorm.DB {
|
||||
return db.
|
||||
Where("e.category = ?", utils.ExpenseCategoryBOP).
|
||||
Joins("LEFT JOIN nonstocks AS n ON n.id = en.nonstock_id").
|
||||
Joins("LEFT JOIN flags AS f ON f.flagable_id = n.id AND f.flagable_type = ? AND f.name = ?", entity.FlagableTypeNonstock, utils.FlagEkspedisi).
|
||||
Where("f.id IS NULL")
|
||||
})
|
||||
}
|
||||
|
||||
func (r *DashboardRepositoryImpl) SumEkspedisiCost(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) (float64, error) {
|
||||
return r.sumExpenseRealization(ctx, start, end, filters, func(db *gorm.DB) *gorm.DB {
|
||||
return db.
|
||||
Joins("JOIN nonstocks AS n ON n.id = en.nonstock_id").
|
||||
Joins("JOIN flags AS f ON f.flagable_id = n.id AND f.flagable_type = ?", entity.FlagableTypeNonstock).
|
||||
Where("f.name = ?", utils.FlagEkspedisi)
|
||||
})
|
||||
}
|
||||
|
||||
func (r *DashboardRepositoryImpl) sumExpenseRealization(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter, modifier func(*gorm.DB) *gorm.DB) (float64, error) {
|
||||
var total float64
|
||||
|
||||
db := r.DB().WithContext(ctx).
|
||||
Table("expense_realizations AS er").
|
||||
Select("COALESCE(SUM(er.qty * er.price), 0) AS total").
|
||||
Joins("JOIN expense_nonstocks AS en ON en.id = er.expense_nonstock_id").
|
||||
Joins("JOIN expenses AS e ON e.id = en.expense_id").
|
||||
Joins("LEFT JOIN project_flock_kandangs AS pfk ON pfk.id = en.project_flock_kandang_id").
|
||||
Joins("LEFT JOIN kandangs AS k ON k.id = COALESCE(en.kandang_id, pfk.kandang_id)").
|
||||
Where("e.realization_date >= ? AND e.realization_date < ?", start, end)
|
||||
|
||||
db = applyDashboardFilters(db, filters)
|
||||
|
||||
if modifier != nil {
|
||||
db = modifier(db)
|
||||
}
|
||||
|
||||
if err := db.Scan(&total).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return total, nil
|
||||
}
|
||||
|
||||
func (r *DashboardRepositoryImpl) standardIDSubquery(filters *validation.DashboardFilter) *gorm.DB {
|
||||
db := r.DB().
|
||||
Table("project_flocks AS pf").
|
||||
Select("DISTINCT pf.production_standard_id").
|
||||
Joins("JOIN project_flock_kandangs AS pfk ON pfk.project_flock_id = pf.id").
|
||||
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
|
||||
Where("pf.production_standard_id > 0")
|
||||
|
||||
if filters != nil {
|
||||
if len(filters.FlockIds) > 0 {
|
||||
db = db.Where("pf.id IN ?", filters.FlockIds)
|
||||
}
|
||||
if len(filters.KandangIds) > 0 {
|
||||
db = db.Where("k.id IN ?", filters.KandangIds)
|
||||
}
|
||||
if len(filters.LokasiIds) > 0 {
|
||||
db = db.Where("k.location_id IN ?", filters.LokasiIds)
|
||||
}
|
||||
}
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
func (r *DashboardRepositoryImpl) standardSourceSubquery(filters *validation.DashboardFilter) *gorm.DB {
|
||||
db := r.DB().
|
||||
Table("project_flocks AS pf").
|
||||
Select("DISTINCT pf.production_standard_id, pf.fcr_id").
|
||||
Joins("JOIN project_flock_kandangs AS pfk ON pfk.project_flock_id = pf.id").
|
||||
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
|
||||
Where("pf.production_standard_id > 0").
|
||||
Where("pf.fcr_id > 0")
|
||||
|
||||
if filters != nil {
|
||||
if len(filters.FlockIds) > 0 {
|
||||
db = db.Where("pf.id IN ?", filters.FlockIds)
|
||||
}
|
||||
if len(filters.KandangIds) > 0 {
|
||||
db = db.Where("k.id IN ?", filters.KandangIds)
|
||||
}
|
||||
if len(filters.LokasiIds) > 0 {
|
||||
db = db.Where("k.location_id IN ?", filters.LokasiIds)
|
||||
}
|
||||
}
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
func (r *DashboardRepositoryImpl) GetComparisonSeries(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter, comparisonType string) ([]ComparisonSeries, error) {
|
||||
seriesExpr, labelExpr, groupExpr, orderExpr, err := comparisonSeriesColumns(comparisonType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var rows []ComparisonSeries
|
||||
db := r.DB().WithContext(ctx).
|
||||
Table("recordings AS r").
|
||||
Select(fmt.Sprintf("%s AS id, %s AS label", seriesExpr, labelExpr)).
|
||||
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id").
|
||||
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
|
||||
Joins("JOIN project_flocks AS pf ON pf.id = pfk.project_flock_id").
|
||||
Joins("JOIN locations AS loc ON loc.id = k.location_id").
|
||||
Where("r.record_datetime >= ? AND r.record_datetime < ?", start, end).
|
||||
Where("r.deleted_at IS NULL")
|
||||
|
||||
db = applyDashboardFilters(db, filters)
|
||||
|
||||
if err := db.Group(groupExpr).Order(orderExpr).Scan(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
func (r *DashboardRepositoryImpl) GetComparisonWeeklyMetrics(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter, comparisonType, metric string) ([]ComparisonWeeklyMetric, error) {
|
||||
seriesExpr, _, groupExpr, orderExpr, err := comparisonSeriesColumns(comparisonType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
metricExpr, err := comparisonMetricColumn(metric)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var rows []ComparisonWeeklyMetric
|
||||
db := r.DB().WithContext(ctx).
|
||||
Table("recordings AS r").
|
||||
Select(fmt.Sprintf(`((r.day - 1) / 7 + 1) AS week,
|
||||
%s AS series_id,
|
||||
COALESCE(AVG(%s), 0) AS value`, seriesExpr, metricExpr)).
|
||||
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id").
|
||||
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
|
||||
Joins("JOIN project_flocks AS pf ON pf.id = pfk.project_flock_id").
|
||||
Joins("JOIN locations AS loc ON loc.id = k.location_id").
|
||||
Where("r.record_datetime >= ? AND r.record_datetime < ?", start, end).
|
||||
Where("r.deleted_at IS NULL").
|
||||
Where("r.day IS NOT NULL AND r.day > 0")
|
||||
|
||||
db = applyDashboardFilters(db, filters)
|
||||
|
||||
groupBy := fmt.Sprintf("week, %s", groupExpr)
|
||||
orderBy := fmt.Sprintf("week ASC, %s", orderExpr)
|
||||
if err := db.Group(groupBy).Order(orderBy).Scan(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
func (r *DashboardRepositoryImpl) GetComparisonWeeklyUniformityMetrics(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter, comparisonType string) ([]ComparisonUniformityMetric, error) {
|
||||
seriesExpr, _, groupExpr, orderExpr, err := comparisonSeriesColumns(comparisonType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var rows []ComparisonUniformityMetric
|
||||
db := r.DB().WithContext(ctx).
|
||||
Table("project_flock_kandang_uniformity AS u").
|
||||
Select(fmt.Sprintf(`u.week AS week,
|
||||
%s AS series_id,
|
||||
COALESCE(AVG(u.uniformity), 0) AS uniformity,
|
||||
COALESCE(AVG((u.chart_data->'statistics'->>'average_weight')::numeric), 0) AS average_weight`, seriesExpr)).
|
||||
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = u.project_flock_kandang_id").
|
||||
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
|
||||
Joins("JOIN project_flocks AS pf ON pf.id = pfk.project_flock_id").
|
||||
Joins("JOIN locations AS loc ON loc.id = k.location_id").
|
||||
Where("u.uniform_date IS NOT NULL").
|
||||
Where("u.uniform_date >= ? AND u.uniform_date < ?", start, end)
|
||||
|
||||
db = applyDashboardFilters(db, filters)
|
||||
|
||||
groupBy := fmt.Sprintf("u.week, %s", groupExpr)
|
||||
orderBy := fmt.Sprintf("u.week ASC, %s", orderExpr)
|
||||
if err := db.Group(groupBy).Order(orderBy).Scan(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
func (r *DashboardRepositoryImpl) GetEggQualityWeeklyMetrics(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) ([]EggQualityWeeklyMetric, error) {
|
||||
var rows []EggQualityWeeklyMetric
|
||||
|
||||
db := r.DB().WithContext(ctx).
|
||||
Table("recording_eggs AS re").
|
||||
Select(`
|
||||
((r.day - 1) / 7 + 1) AS week,
|
||||
COALESCE(SUM(CASE WHEN f.name = ? THEN re.qty ELSE 0 END), 0) AS normal_qty,
|
||||
COALESCE(SUM(CASE WHEN f.name IN (?, ?, ?) THEN re.qty ELSE 0 END), 0) AS abnormal_qty,
|
||||
COALESCE(SUM(re.qty), 0) AS total_qty`,
|
||||
utils.FlagTelurUtuh,
|
||||
utils.FlagTelurPutih,
|
||||
utils.FlagTelurRetak,
|
||||
utils.FlagTelurPecah,
|
||||
).
|
||||
Joins("JOIN recordings AS r ON r.id = re.recording_id").
|
||||
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id").
|
||||
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
|
||||
Joins("JOIN product_warehouses AS pw ON pw.id = re.product_warehouse_id").
|
||||
Joins("JOIN products AS p ON p.id = pw.product_id").
|
||||
Joins("JOIN flags AS f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct).
|
||||
Where("f.name IN ?", []utils.FlagType{utils.FlagTelurUtuh, utils.FlagTelurPutih, utils.FlagTelurRetak, utils.FlagTelurPecah}).
|
||||
Where("r.record_datetime >= ? AND r.record_datetime < ?", start, end).
|
||||
Where("r.deleted_at IS NULL").
|
||||
Where("r.day IS NOT NULL AND r.day > 0")
|
||||
|
||||
db = applyDashboardFilters(db, filters)
|
||||
|
||||
if err := db.Group("week").Order("week ASC").Scan(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
func (r *DashboardRepositoryImpl) GetEggWeightWeeklyGrams(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) ([]WeeklyEggWeightMetric, error) {
|
||||
var rows []WeeklyEggWeightMetric
|
||||
|
||||
db := r.DB().WithContext(ctx).
|
||||
Table("recording_eggs AS re").
|
||||
Select(`
|
||||
((r.day - 1) / 7 + 1) AS week,
|
||||
COALESCE(SUM(re.qty * re.weight), 0) AS egg_weight_grams`).
|
||||
Joins("JOIN recordings AS r ON r.id = re.recording_id").
|
||||
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id").
|
||||
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
|
||||
Where("r.record_datetime >= ? AND r.record_datetime < ?", start, end).
|
||||
Where("r.deleted_at IS NULL").
|
||||
Where("r.day IS NOT NULL AND r.day > 0")
|
||||
|
||||
db = applyDashboardFilters(db, filters)
|
||||
|
||||
if err := db.Group("week").Order("week ASC").Scan(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
func (r *DashboardRepositoryImpl) GetFeedUsageWeeklyByUom(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) ([]WeeklyFeedUsageMetric, error) {
|
||||
var rows []WeeklyFeedUsageMetric
|
||||
|
||||
db := r.DB().WithContext(ctx).
|
||||
Table("recording_stocks AS rs").
|
||||
Select(`
|
||||
((r.day - 1) / 7 + 1) AS week,
|
||||
COALESCE(SUM(rs.usage_qty), 0) + COALESCE(SUM(rs.pending_qty), 0) AS total_qty,
|
||||
LOWER(uoms.name) AS uom_name`).
|
||||
Joins("JOIN recordings AS r ON r.id = rs.recording_id").
|
||||
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id").
|
||||
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
|
||||
Joins("JOIN product_warehouses AS pw ON pw.id = rs.product_warehouse_id").
|
||||
Joins("JOIN products AS p ON p.id = pw.product_id").
|
||||
Joins("JOIN uoms ON uoms.id = p.uom_id").
|
||||
Joins("JOIN flags AS f ON f.flagable_id = p.id AND f.flagable_type = ? AND UPPER(f.name) = ?", entity.FlagableTypeProduct, "PAKAN").
|
||||
Where("r.record_datetime >= ? AND r.record_datetime < ?", start, end).
|
||||
Where("r.deleted_at IS NULL").
|
||||
Where("r.day IS NOT NULL AND r.day > 0")
|
||||
|
||||
db = applyDashboardFilters(db, filters)
|
||||
|
||||
if err := db.Group("week, LOWER(uoms.name)").Order("week ASC").Scan(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
func comparisonSeriesColumns(comparisonType string) (string, string, string, string, error) {
|
||||
switch strings.ToUpper(strings.TrimSpace(comparisonType)) {
|
||||
case validation.ComparisonTypeFarm:
|
||||
return "loc.id", "loc.name", "loc.id, loc.name", "loc.name", nil
|
||||
case validation.ComparisonTypeFlock:
|
||||
return "pf.id", "pf.flock_name", "pf.id, pf.flock_name", "pf.flock_name", nil
|
||||
case validation.ComparisonTypeKandang:
|
||||
return "k.id", "k.name", "k.id, k.name", "k.name", nil
|
||||
default:
|
||||
return "", "", "", "", fmt.Errorf("invalid comparison_type")
|
||||
}
|
||||
}
|
||||
|
||||
func comparisonMetricColumn(metric string) (string, error) {
|
||||
switch strings.ToLower(strings.TrimSpace(metric)) {
|
||||
case validation.MetricFcr:
|
||||
return "r.fcr_value", nil
|
||||
case validation.MetricMortality:
|
||||
return "r.cum_depletion_rate", nil
|
||||
case validation.MetricLaying:
|
||||
return "r.hen_day", nil
|
||||
case validation.MetricEggWeight:
|
||||
return "r.egg_weight", nil
|
||||
case validation.MetricFeedIntake:
|
||||
return "r.feed_intake", nil
|
||||
default:
|
||||
return "", fmt.Errorf("invalid metric")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package dashboards
|
||||
|
||||
import (
|
||||
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/dashboards/controllers"
|
||||
dashboard "gitlab.com/mbugroup/lti-api.git/internal/modules/dashboards/services"
|
||||
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func DashboardRoutes(v1 fiber.Router, u user.UserService, s dashboard.DashboardService) {
|
||||
ctrl := controller.NewDashboardController(s)
|
||||
|
||||
route := v1.Group("/dashboards")
|
||||
route.Use(m.Auth(u))
|
||||
|
||||
// route.Get("/", m.RequirePermissions(m.P_FinanceGetAll), ctrl.GetAll)
|
||||
// route.Post("/", m.RequirePermissions(m.P_FinanceGetOne), ctrl.CreateOne)
|
||||
// route.Get("/:id", m.RequirePermissions(m.P_FinanceCreateOne), ctrl.GetOne)
|
||||
// route.Patch("/:id", m.RequirePermissions(m.P_FinanceUpdateOne), ctrl.UpdateOne)
|
||||
// route.Delete("/:id", m.RequirePermissions(m.P_FinanceDeleteOne), ctrl.DeleteOne)
|
||||
|
||||
route.Get("/", ctrl.GetAll)
|
||||
// route.Get("/:id", ctrl.GetOne)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package validation
|
||||
|
||||
import "time"
|
||||
|
||||
type Create struct {
|
||||
Name string `json:"name" validate:"required_strict,min=3"`
|
||||
}
|
||||
|
||||
const (
|
||||
AnalysisModeOverview = "OVERVIEW"
|
||||
AnalysisModeComparison = "COMPARASION"
|
||||
|
||||
ComparisonTypeFarm = "FARM"
|
||||
ComparisonTypeFlock = "FLOCK"
|
||||
ComparisonTypeKandang = "KANDANG"
|
||||
|
||||
MetricFcr = "fcr"
|
||||
MetricMortality = "mortality"
|
||||
MetricLaying = "laying"
|
||||
MetricEggWeight = "egg_weight"
|
||||
MetricFeedIntake = "feed_intake"
|
||||
)
|
||||
|
||||
type Update struct {
|
||||
Name *string `json:"name,omitempty" validate:"omitempty"`
|
||||
}
|
||||
|
||||
type Query struct {
|
||||
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
|
||||
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
|
||||
Search string `query:"search" validate:"omitempty,max=50"`
|
||||
PerformanceOverviewFilter
|
||||
PeriodStart time.Time `json:"-" query:"-"`
|
||||
PeriodEnd time.Time `json:"-" query:"-"`
|
||||
PeriodEndExclusive time.Time `json:"-" query:"-"`
|
||||
}
|
||||
|
||||
type PerformanceOverviewFilter struct {
|
||||
StartDate string `query:"start_date" validate:"omitempty,datetime=2006-01-02"`
|
||||
EndDate string `query:"end_date" validate:"omitempty,datetime=2006-01-02"`
|
||||
AnalysisMode string `query:"analysis_mode" validate:"omitempty,oneof=OVERVIEW COMPARASION"`
|
||||
ComparisonType string `query:"comparison_type" validate:"omitempty,oneof=FARM FLOCK KANDANG"`
|
||||
Metric string `query:"metric" validate:"omitempty,oneof=fcr mortality laying egg_weight feed_intake"`
|
||||
LokasiIds []uint `query:"lokasi_ids" validate:"omitempty,dive,gt=0"`
|
||||
FlockIds []uint `query:"flock_ids" validate:"omitempty,dive,gt=0"`
|
||||
KandangIds []uint `query:"kandang_ids" validate:"omitempty,dive,gt=0"`
|
||||
Include []string `query:"include" validate:"omitempty,dive,oneof=statistics charts"`
|
||||
}
|
||||
|
||||
type DashboardFilter struct {
|
||||
LokasiIds []uint
|
||||
FlockIds []uint
|
||||
KandangIds []uint
|
||||
}
|
||||
@@ -29,6 +29,14 @@ type SuccessWithPaginate[T any] struct {
|
||||
Data []T `json:"data"`
|
||||
}
|
||||
|
||||
type SuccessWithMeta struct {
|
||||
Code int `json:"code"`
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message"`
|
||||
Meta Meta `json:"meta"`
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
|
||||
type ErrorDetails struct {
|
||||
Code int `json:"code"`
|
||||
Status string `json:"status"`
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
repports "gitlab.com/mbugroup/lti-api.git/internal/modules/repports"
|
||||
ssoModule "gitlab.com/mbugroup/lti-api.git/internal/modules/sso"
|
||||
users "gitlab.com/mbugroup/lti-api.git/internal/modules/users"
|
||||
dashboards "gitlab.com/mbugroup/lti-api.git/internal/modules/dashboards"
|
||||
// MODULE IMPORTS
|
||||
)
|
||||
|
||||
@@ -48,6 +49,7 @@ func Routes(app *fiber.App, db *gorm.DB) {
|
||||
repports.RepportModule{},
|
||||
finance.FinanceModule{},
|
||||
dailyChecklists.DailyChecklistModule{},
|
||||
dashboards.DashboardModule{},
|
||||
// MODULE REGISTRY
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user