mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
Merge branch 'feat/BE/US-390-Dashboard' into 'development'
[FEAT/BE][US-390 dashboard calculation standart] See merge request mbugroup/lti-api!152
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"`
|
||||||
|
}
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
package middleware
|
package middleware
|
||||||
|
|
||||||
|
const(
|
||||||
|
P_DashboardGetAll = "lti.dashboard.list"
|
||||||
|
)
|
||||||
// project-flock
|
// project-flock
|
||||||
const (
|
const (
|
||||||
P_ProjectFlockKandangsClosing = "lti.production.project_flock_kandangs.closing"
|
P_ProjectFlockKandangsClosing = "lti.production.project_flock_kandangs.closing"
|
||||||
|
|||||||
@@ -0,0 +1,206 @@
|
|||||||
|
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("location_ids", ""))
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid location_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
|
||||||
|
}
|
||||||
|
|
||||||
|
hasFilter := query.StartDate != "" ||
|
||||||
|
query.EndDate != "" ||
|
||||||
|
len(query.LokasiIds) > 0 ||
|
||||||
|
len(query.FlockIds) > 0 ||
|
||||||
|
len(query.KandangIds) > 0 ||
|
||||||
|
len(query.Include) > 0 ||
|
||||||
|
query.ComparisonType != "" ||
|
||||||
|
query.Metric != "" ||
|
||||||
|
query.AnalysisMode != validation.AnalysisModeOverview
|
||||||
|
|
||||||
|
var filters interface{}
|
||||||
|
if hasFilter {
|
||||||
|
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:"location_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,725 @@
|
|||||||
|
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
|
||||||
|
HenDay 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 hen_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
|
||||||
|
}
|
||||||
|
|
||||||
|
filterClause := ""
|
||||||
|
filterArgs := make([]interface{}, 0)
|
||||||
|
if filters != nil {
|
||||||
|
if len(filters.FlockIds) > 0 {
|
||||||
|
filterClause += " AND pf.id IN ?"
|
||||||
|
filterArgs = append(filterArgs, filters.FlockIds)
|
||||||
|
}
|
||||||
|
if len(filters.KandangIds) > 0 {
|
||||||
|
filterClause += " AND k.id IN ?"
|
||||||
|
filterArgs = append(filterArgs, filters.KandangIds)
|
||||||
|
}
|
||||||
|
if len(filters.LokasiIds) > 0 {
|
||||||
|
filterClause += " AND k.location_id IN ?"
|
||||||
|
filterArgs = append(filterArgs, filters.LokasiIds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
query := fmt.Sprintf(`
|
||||||
|
WITH src AS (
|
||||||
|
SELECT DISTINCT pf.production_standard_id, pf.fcr_id
|
||||||
|
FROM project_flocks pf
|
||||||
|
JOIN project_flock_kandangs pfk ON pfk.project_flock_id = pf.id
|
||||||
|
JOIN kandangs k ON k.id = pfk.kandang_id
|
||||||
|
WHERE pf.production_standard_id > 0 AND pf.fcr_id > 0
|
||||||
|
%s
|
||||||
|
),
|
||||||
|
actual AS (
|
||||||
|
SELECT u.week AS week,
|
||||||
|
pf.fcr_id AS fcr_id,
|
||||||
|
AVG((u.chart_data->'statistics'->>'average_weight')::numeric) AS avg_weight
|
||||||
|
FROM project_flock_kandang_uniformity u
|
||||||
|
JOIN project_flock_kandangs pfk ON pfk.id = u.project_flock_kandang_id
|
||||||
|
JOIN project_flocks pf ON pf.id = pfk.project_flock_id
|
||||||
|
JOIN kandangs k ON k.id = pfk.kandang_id
|
||||||
|
WHERE u.week IN ? AND u.uniform_date IS NOT NULL AND pf.fcr_id > 0
|
||||||
|
%s
|
||||||
|
GROUP BY u.week, pf.fcr_id
|
||||||
|
),
|
||||||
|
target AS (
|
||||||
|
SELECT sgd.week AS week,
|
||||||
|
src.fcr_id AS fcr_id,
|
||||||
|
AVG(sgd.target_mean_bw) AS target_mean_bw
|
||||||
|
FROM standard_growth_details sgd
|
||||||
|
JOIN src ON src.production_standard_id = sgd.production_standard_id
|
||||||
|
WHERE sgd.week IN ?
|
||||||
|
GROUP BY sgd.week, src.fcr_id
|
||||||
|
),
|
||||||
|
weights AS (
|
||||||
|
SELECT COALESCE(a.week, t.week) AS week,
|
||||||
|
COALESCE(a.fcr_id, t.fcr_id) AS fcr_id,
|
||||||
|
COALESCE(
|
||||||
|
CASE WHEN a.avg_weight > 10 THEN a.avg_weight / 1000 ELSE a.avg_weight END,
|
||||||
|
CASE WHEN t.target_mean_bw > 10 THEN t.target_mean_bw / 1000 ELSE t.target_mean_bw END
|
||||||
|
) AS weight
|
||||||
|
FROM actual a
|
||||||
|
FULL OUTER JOIN target t ON t.week = a.week AND t.fcr_id = a.fcr_id
|
||||||
|
)
|
||||||
|
SELECT w.week AS week,
|
||||||
|
COALESCE(AVG(
|
||||||
|
COALESCE(
|
||||||
|
(SELECT fs.fcr_number
|
||||||
|
FROM fcr_standards fs
|
||||||
|
WHERE fs.fcr_id = w.fcr_id
|
||||||
|
AND fs.weight >= w.weight
|
||||||
|
ORDER BY fs.weight ASC
|
||||||
|
LIMIT 1),
|
||||||
|
(SELECT fs.fcr_number
|
||||||
|
FROM fcr_standards fs
|
||||||
|
WHERE fs.fcr_id = w.fcr_id
|
||||||
|
ORDER BY fs.weight DESC
|
||||||
|
LIMIT 1)
|
||||||
|
)
|
||||||
|
), 0) AS std_fcr
|
||||||
|
FROM weights w
|
||||||
|
GROUP BY w.week
|
||||||
|
ORDER BY w.week ASC
|
||||||
|
`, filterClause, filterClause)
|
||||||
|
|
||||||
|
args := make([]interface{}, 0, len(filterArgs)*2+2)
|
||||||
|
args = append(args, filterArgs...)
|
||||||
|
args = append(args, weeks)
|
||||||
|
args = append(args, filterArgs...)
|
||||||
|
args = append(args, weeks)
|
||||||
|
|
||||||
|
var rows []StandardWeeklyFcrMetric
|
||||||
|
if err := r.DB().WithContext(ctx).Raw(query, args...).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,18 @@
|
|||||||
|
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_DashboardGetAll) ,ctrl.GetAll)
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,54 @@
|
|||||||
|
package validation
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type Create struct {
|
||||||
|
Name string `json:"name" validate:"required_strict,min=3"`
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
AnalysisModeOverview = "OVERVIEW"
|
||||||
|
AnalysisModeComparison = "COMPARISON"
|
||||||
|
|
||||||
|
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 COMPARISON"`
|
||||||
|
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:"location_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"`
|
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 {
|
type ErrorDetails struct {
|
||||||
Code int `json:"code"`
|
Code int `json:"code"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import (
|
|||||||
repports "gitlab.com/mbugroup/lti-api.git/internal/modules/repports"
|
repports "gitlab.com/mbugroup/lti-api.git/internal/modules/repports"
|
||||||
ssoModule "gitlab.com/mbugroup/lti-api.git/internal/modules/sso"
|
ssoModule "gitlab.com/mbugroup/lti-api.git/internal/modules/sso"
|
||||||
users "gitlab.com/mbugroup/lti-api.git/internal/modules/users"
|
users "gitlab.com/mbugroup/lti-api.git/internal/modules/users"
|
||||||
|
dashboards "gitlab.com/mbugroup/lti-api.git/internal/modules/dashboards"
|
||||||
// MODULE IMPORTS
|
// MODULE IMPORTS
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -48,6 +49,7 @@ func Routes(app *fiber.App, db *gorm.DB) {
|
|||||||
repports.RepportModule{},
|
repports.RepportModule{},
|
||||||
finance.FinanceModule{},
|
finance.FinanceModule{},
|
||||||
dailyChecklists.DailyChecklistModule{},
|
dailyChecklists.DailyChecklistModule{},
|
||||||
|
dashboards.DashboardModule{},
|
||||||
// MODULE REGISTRY
|
// MODULE REGISTRY
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user