mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-24 15:25:43 +00:00
Merge branch 'development' of https://gitlab.com/mbugroup/lti-api into dev/teguh
This commit is contained in:
@@ -13,6 +13,7 @@ func FiberConfig() fiber.Config {
|
|||||||
CaseSensitive: true,
|
CaseSensitive: true,
|
||||||
ServerHeader: "Fiber",
|
ServerHeader: "Fiber",
|
||||||
AppName: "Fiber API",
|
AppName: "Fiber API",
|
||||||
|
BodyLimit: 8 * 1024 * 1024,
|
||||||
ErrorHandler: utils.ErrorHandler,
|
ErrorHandler: utils.ErrorHandler,
|
||||||
JSONEncoder: sonic.Marshal,
|
JSONEncoder: sonic.Marshal,
|
||||||
JSONDecoder: sonic.Unmarshal,
|
JSONDecoder: sonic.Unmarshal,
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
-- Remove chart_data, uniform_date, and related indexes
|
||||||
|
DROP INDEX IF EXISTS idx_project_flock_kandang_uniformity_uniform_date;
|
||||||
|
DROP INDEX IF EXISTS idx_project_flock_kandang_uniformity_unique;
|
||||||
|
|
||||||
|
ALTER TABLE project_flock_kandang_uniformity
|
||||||
|
DROP COLUMN IF EXISTS chart_data,
|
||||||
|
DROP COLUMN IF EXISTS uniform_date;
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
-- Add uniform_date (if missing), chart_data, and unique constraint for uniformity records
|
||||||
|
ALTER TABLE project_flock_kandang_uniformity
|
||||||
|
ADD COLUMN IF NOT EXISTS uniform_date TIMESTAMPTZ,
|
||||||
|
ADD COLUMN IF NOT EXISTS chart_data JSONB;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_schema = 'public'
|
||||||
|
AND table_name = 'project_flock_kandang_uniformity'
|
||||||
|
AND column_name = 'deleted_at'
|
||||||
|
) THEN
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_project_flock_kandang_uniformity_unique
|
||||||
|
ON project_flock_kandang_uniformity (project_flock_kandang_id, week, uniform_date)
|
||||||
|
WHERE deleted_at IS NULL;
|
||||||
|
ELSE
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_project_flock_kandang_uniformity_unique
|
||||||
|
ON project_flock_kandang_uniformity (project_flock_kandang_id, week, uniform_date);
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_project_flock_kandang_uniformity_uniform_date
|
||||||
|
ON project_flock_kandang_uniformity (uniform_date);
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE IF EXISTS config_checklists;
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS config_checklists (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
date DATE NOT NULL,
|
||||||
|
percentage_threshold_bad INTEGER NOT NULL,
|
||||||
|
percentage_threshold_enough INTEGER NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
deleted_at TIMESTAMPTZ
|
||||||
|
);
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package entities
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ConfigChecklist struct {
|
||||||
|
Id uint `gorm:"primaryKey"`
|
||||||
|
Date time.Time `gorm:"type:date;not null"`
|
||||||
|
PercentageThresholdBad int `gorm:"not null"`
|
||||||
|
PercentageThresholdEnough int `gorm:"not null"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
|
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||||
|
}
|
||||||
@@ -1,20 +1,24 @@
|
|||||||
package entities
|
package entities
|
||||||
|
|
||||||
import "time"
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
type ProjectFlockKandangUniformity struct {
|
type ProjectFlockKandangUniformity struct {
|
||||||
Id uint `gorm:"primaryKey"`
|
Id uint `gorm:"primaryKey"`
|
||||||
Uniformity float64 `gorm:"type:numeric(15,3)"`
|
Uniformity float64 `gorm:"type:numeric(15,3)"`
|
||||||
Week int `gorm:"not null"`
|
Week int `gorm:"not null"`
|
||||||
Cv float64 `gorm:"type:numeric(15,3)"`
|
Cv float64 `gorm:"type:numeric(15,3)"`
|
||||||
ChickQtyOfWeight float64 `gorm:"type:numeric(15,3)"`
|
ChickQtyOfWeight float64 `gorm:"type:numeric(15,3)"`
|
||||||
MeanUp float64 `gorm:"type:numeric(15,3)"`
|
MeanUp float64 `gorm:"type:numeric(15,3)"`
|
||||||
MeanDown float64 `gorm:"type:numeric(15,3)"`
|
MeanDown float64 `gorm:"type:numeric(15,3)"`
|
||||||
ProjectFlockKandangId uint `gorm:"not null"`
|
ProjectFlockKandangId uint `gorm:"not null"`
|
||||||
UniformQty float64 `gorm:"type:numeric(15,3)"`
|
UniformQty float64 `gorm:"type:numeric(15,3)"`
|
||||||
NotUniformQty float64 `gorm:"type:numeric(15,3)"`
|
NotUniformQty float64 `gorm:"type:numeric(15,3)"`
|
||||||
UniformDate *time.Time `gorm:"type:timestamptz"`
|
ChartData json.RawMessage `gorm:"type:jsonb"`
|
||||||
CreatedBy uint `gorm:"not null"`
|
UniformDate *time.Time `gorm:"type:timestamptz"`
|
||||||
|
CreatedBy uint `gorm:"not null"`
|
||||||
|
|
||||||
ProjectFlockKandang ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
|
ProjectFlockKandang ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
|
||||||
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ type RecordingEgg struct {
|
|||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
|
ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
|
||||||
ProductFlagName *string `gorm:"column:product_flag_name" json:"-"`
|
ProductFlagName *string `gorm:"->;column:product_flag_name" json:"-"`
|
||||||
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
|
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||||
Recording Recording `gorm:"foreignKey:RecordingId;references:Id"`
|
Recording Recording `gorm:"foreignKey:RecordingId;references:Id"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/dto"
|
"gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/dto"
|
||||||
service "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/services"
|
service "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/services"
|
||||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/validations"
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/validations"
|
||||||
|
kandangDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/response"
|
"gitlab.com/mbugroup/lti-api.git/internal/response"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
@@ -28,6 +29,18 @@ func (u *DailyChecklistController) GetAll(c *fiber.Ctx) error {
|
|||||||
Limit: c.QueryInt("limit", 10),
|
Limit: c.QueryInt("limit", 10),
|
||||||
Search: c.Query("search", ""),
|
Search: c.Query("search", ""),
|
||||||
}
|
}
|
||||||
|
query.DateFrom = c.Query("date_from", "")
|
||||||
|
query.DateTo = c.Query("date_to", "")
|
||||||
|
query.Status = c.Query("status", "")
|
||||||
|
|
||||||
|
if kandangParam := c.Query("kandang_id", ""); kandangParam != "" {
|
||||||
|
kandangID, err := strconv.ParseUint(kandangParam, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid kandang_id")
|
||||||
|
}
|
||||||
|
value := uint(kandangID)
|
||||||
|
query.KandangID = &value
|
||||||
|
}
|
||||||
|
|
||||||
if query.Page < 1 || query.Limit < 1 {
|
if query.Page < 1 || query.Limit < 1 {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
|
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
|
||||||
@@ -38,6 +51,40 @@ func (u *DailyChecklistController) GetAll(c *fiber.Ctx) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
responseData := make([]dto.DailyChecklistListDTO, len(result))
|
||||||
|
for i, item := range result {
|
||||||
|
var name string
|
||||||
|
if item.Name != nil {
|
||||||
|
name = *item.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
var status string
|
||||||
|
if item.Status != nil {
|
||||||
|
status = *item.Status
|
||||||
|
}
|
||||||
|
|
||||||
|
var kandang *kandangDTO.KandangRelationDTO
|
||||||
|
if item.Kandang.Id != 0 {
|
||||||
|
mapped := kandangDTO.ToKandangRelationDTO(item.Kandang)
|
||||||
|
kandang = &mapped
|
||||||
|
}
|
||||||
|
|
||||||
|
responseData[i] = dto.DailyChecklistListDTO{
|
||||||
|
Id: item.ID,
|
||||||
|
Name: name,
|
||||||
|
Status: status,
|
||||||
|
Category: item.Category,
|
||||||
|
Date: item.Date,
|
||||||
|
Kandang: kandang,
|
||||||
|
CreatedUser: nil,
|
||||||
|
CreatedAt: item.CreatedAt,
|
||||||
|
UpdatedAt: item.UpdatedAt,
|
||||||
|
TotalPhase: item.TotalPhase,
|
||||||
|
TotalActivity: item.TotalActivity,
|
||||||
|
Progress: item.Progress,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return c.Status(fiber.StatusOK).
|
return c.Status(fiber.StatusOK).
|
||||||
JSON(response.SuccessWithPaginate[dto.DailyChecklistListDTO]{
|
JSON(response.SuccessWithPaginate[dto.DailyChecklistListDTO]{
|
||||||
Code: fiber.StatusOK,
|
Code: fiber.StatusOK,
|
||||||
@@ -49,19 +96,209 @@ func (u *DailyChecklistController) GetAll(c *fiber.Ctx) error {
|
|||||||
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
|
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
|
||||||
TotalResults: totalResults,
|
TotalResults: totalResults,
|
||||||
},
|
},
|
||||||
Data: dto.ToDailyChecklistListDTOs(result),
|
Data: responseData,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *DailyChecklistController) GetSummary(c *fiber.Ctx) error {
|
||||||
|
query := &validation.SummaryQuery{
|
||||||
|
DateFrom: c.Query("date_from"),
|
||||||
|
DateTo: c.Query("date_to"),
|
||||||
|
Category: c.Query("category"),
|
||||||
|
}
|
||||||
|
|
||||||
|
if query.DateFrom == "" || query.DateTo == "" {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "date_from and date_to are required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if kandangParam := c.Query("kandang_id"); kandangParam != "" {
|
||||||
|
kandangID, err := strconv.ParseUint(kandangParam, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid kandang_id")
|
||||||
|
}
|
||||||
|
value := uint(kandangID)
|
||||||
|
query.KandangID = &value
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := u.DailyChecklistService.GetSummary(c, query)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
type summaryResponse struct {
|
||||||
|
PerformanceOverview []dto.DailyChecklistPerformanceOverviewDTO `json:"performance_overview"`
|
||||||
|
TrackingABK []dto.DailyChecklistSummaryDTO `json:"tracking_abk"`
|
||||||
|
}
|
||||||
|
|
||||||
|
performanceMap := make(map[uint]*dto.DailyChecklistPerformanceOverviewDTO)
|
||||||
|
tracking := make([]dto.DailyChecklistSummaryDTO, len(result))
|
||||||
|
|
||||||
|
for i, summary := range result {
|
||||||
|
tracking[i] = dto.DailyChecklistSummaryDTO{
|
||||||
|
EmployeeID: summary.EmployeeID,
|
||||||
|
EmployeeName: summary.EmployeeName,
|
||||||
|
KandangID: summary.KandangID,
|
||||||
|
KandangName: summary.KandangName,
|
||||||
|
TotalActivity: summary.TotalActivity,
|
||||||
|
ActivityDone: summary.ActivityDone,
|
||||||
|
ActivityLeft: summary.ActivityLeft,
|
||||||
|
CompletionRate: summary.CompletionRate,
|
||||||
|
LastActivity: summary.LastActivity,
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := performanceMap[summary.EmployeeID]; !ok {
|
||||||
|
performanceMap[summary.EmployeeID] = &dto.DailyChecklistPerformanceOverviewDTO{
|
||||||
|
EmployeeID: summary.EmployeeID,
|
||||||
|
EmployeeName: summary.EmployeeName,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
performanceMap[summary.EmployeeID].TotalActivity += summary.TotalActivity
|
||||||
|
performanceMap[summary.EmployeeID].ActivityDone += summary.ActivityDone
|
||||||
|
performanceMap[summary.EmployeeID].ActivityLeft += summary.ActivityLeft
|
||||||
|
}
|
||||||
|
|
||||||
|
performance := make([]dto.DailyChecklistPerformanceOverviewDTO, 0, len(performanceMap))
|
||||||
|
for _, v := range performanceMap {
|
||||||
|
performance = append(performance, *v)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.Success{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Get daily checklist summary successfully",
|
||||||
|
Data: summaryResponse{
|
||||||
|
PerformanceOverview: performance,
|
||||||
|
TrackingABK: tracking,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *DailyChecklistController) GetReport(c *fiber.Ctx) error {
|
||||||
|
query := &validation.ReportQuery{
|
||||||
|
Page: c.QueryInt("page", 1),
|
||||||
|
Limit: c.QueryInt("limit", 10),
|
||||||
|
Month: c.QueryInt("bulan", 0),
|
||||||
|
Year: c.QueryInt("tahun", 0),
|
||||||
|
}
|
||||||
|
|
||||||
|
parseUintParam := func(param string) (*uint, error) {
|
||||||
|
if param == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
value, err := strconv.ParseUint(param, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
u := uint(value)
|
||||||
|
return &u, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if val, err := parseUintParam(c.Query("area_id", "")); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid area_id")
|
||||||
|
} else {
|
||||||
|
query.AreaID = val
|
||||||
|
}
|
||||||
|
|
||||||
|
if val, err := parseUintParam(c.Query("location_id", "")); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid location_id")
|
||||||
|
} else {
|
||||||
|
query.LocationID = val
|
||||||
|
}
|
||||||
|
|
||||||
|
if val, err := parseUintParam(c.Query("kandang_id", "")); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid kandang_id")
|
||||||
|
} else {
|
||||||
|
query.KandangID = val
|
||||||
|
}
|
||||||
|
|
||||||
|
if val, err := parseUintParam(c.Query("employee_id", "")); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid employee_id")
|
||||||
|
} else {
|
||||||
|
query.EmployeeID = val
|
||||||
|
}
|
||||||
|
|
||||||
|
if val, err := parseUintParam(c.Query("phase_id", "")); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid phase_id")
|
||||||
|
} else {
|
||||||
|
query.PhaseID = val
|
||||||
|
}
|
||||||
|
|
||||||
|
if query.Month == 0 || query.Year == 0 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "bulan and tahun are required")
|
||||||
|
}
|
||||||
|
|
||||||
|
result, totalResults, err := u.DailyChecklistService.GetReport(c, query)
|
||||||
|
withoutActivities := func(src map[string]int) map[string]int {
|
||||||
|
if src == nil {
|
||||||
|
return map[string]int{}
|
||||||
|
}
|
||||||
|
return src
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
responseData := make([]dto.DailyChecklistReportDTO, len(result))
|
||||||
|
for i, item := range result {
|
||||||
|
responseData[i] = dto.DailyChecklistReportDTO{
|
||||||
|
Area: dto.DailyChecklistReportEntityDTO{
|
||||||
|
Id: item.AreaID,
|
||||||
|
Name: item.AreaName,
|
||||||
|
},
|
||||||
|
Farm: dto.DailyChecklistReportEntityDTO{
|
||||||
|
Id: item.LocationID,
|
||||||
|
Name: item.LocationName,
|
||||||
|
},
|
||||||
|
Kandang: dto.DailyChecklistReportEntityDTO{
|
||||||
|
Id: item.KandangID,
|
||||||
|
Name: item.KandangName,
|
||||||
|
},
|
||||||
|
ABK: dto.DailyChecklistReportEntityDTO{
|
||||||
|
Id: item.EmployeeID,
|
||||||
|
Name: item.EmployeeName,
|
||||||
|
},
|
||||||
|
Phase: item.PhaseName,
|
||||||
|
DailyActivities: withoutActivities(item.DailyActivities),
|
||||||
|
Summary: dto.DailyChecklistReportSummaryDTO{
|
||||||
|
TotalChecklist: item.Summary.TotalChecklist,
|
||||||
|
JumlahHariEfektif: item.Summary.JumlahHariEfektif,
|
||||||
|
AbkPercentage: item.Summary.AbkPercentage,
|
||||||
|
KandangPercentage: item.Summary.KandangPercentage,
|
||||||
|
Kategori: dto.DailyChecklistReportCategoryDTO{
|
||||||
|
Kurang: item.Summary.Category.Kurang,
|
||||||
|
Cukup: item.Summary.Category.Cukup,
|
||||||
|
Baik: item.Summary.Category.Baik,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.SuccessWithPaginate[dto.DailyChecklistReportDTO]{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Get daily checklist report successfully",
|
||||||
|
Meta: response.Meta{
|
||||||
|
Page: query.Page,
|
||||||
|
Limit: query.Limit,
|
||||||
|
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
|
||||||
|
TotalResults: totalResults,
|
||||||
|
},
|
||||||
|
Data: responseData,
|
||||||
|
})
|
||||||
|
}
|
||||||
func (u *DailyChecklistController) GetOne(c *fiber.Ctx) error {
|
func (u *DailyChecklistController) GetOne(c *fiber.Ctx) error {
|
||||||
param := c.Params("id")
|
param := c.Params("idDailyChecklist")
|
||||||
|
|
||||||
id, err := strconv.Atoi(param)
|
id, err := strconv.Atoi(param)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := u.DailyChecklistService.GetOne(c, uint(id))
|
detail, err := u.DailyChecklistService.GetDetail(c, uint(id))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -71,7 +308,7 @@ func (u *DailyChecklistController) GetOne(c *fiber.Ctx) error {
|
|||||||
Code: fiber.StatusOK,
|
Code: fiber.StatusOK,
|
||||||
Status: "success",
|
Status: "success",
|
||||||
Message: "Get dailyChecklist successfully",
|
Message: "Get dailyChecklist successfully",
|
||||||
Data: dto.ToDailyChecklistListDTO(*result),
|
Data: dto.ToDailyChecklistDetailDTO(detail.Checklist, detail.Phases, detail.Tasks, detail.AssignedEmployees, detail.TotalActivities, detail.Progress),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,7 +335,7 @@ func (u *DailyChecklistController) CreateOne(c *fiber.Ctx) error {
|
|||||||
|
|
||||||
func (u *DailyChecklistController) UpdateOne(c *fiber.Ctx) error {
|
func (u *DailyChecklistController) UpdateOne(c *fiber.Ctx) error {
|
||||||
req := new(validation.Update)
|
req := new(validation.Update)
|
||||||
param := c.Params("id")
|
param := c.Params("idDailyChecklist")
|
||||||
|
|
||||||
id, err := strconv.Atoi(param)
|
id, err := strconv.Atoi(param)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -124,7 +361,7 @@ func (u *DailyChecklistController) UpdateOne(c *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (u *DailyChecklistController) DeleteOne(c *fiber.Ctx) error {
|
func (u *DailyChecklistController) DeleteOne(c *fiber.Ctx) error {
|
||||||
param := c.Params("id")
|
param := c.Params("idDailyChecklist")
|
||||||
|
|
||||||
id, err := strconv.Atoi(param)
|
id, err := strconv.Atoi(param)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -217,6 +454,32 @@ func (u *DailyChecklistController) RemoveAssignment(c *fiber.Ctx) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *DailyChecklistController) GetPhaseByIdChecklist(c *fiber.Ctx) error {
|
||||||
|
param := c.Params("idDailyChecklist")
|
||||||
|
id, err := strconv.Atoi(param)
|
||||||
|
if err != nil || id <= 0 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid daily checklist id")
|
||||||
|
}
|
||||||
|
|
||||||
|
phaseIDs, err := u.DailyChecklistService.GetChecklistPhaseIDs(c, uint(id))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
responseData := make([]map[string]uint, len(phaseIDs))
|
||||||
|
for i, phaseID := range phaseIDs {
|
||||||
|
responseData[i] = map[string]uint{"phase_id": phaseID}
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.Success{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Get phases successfully",
|
||||||
|
Data: responseData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (u *DailyChecklistController) GetAllTasks(c *fiber.Ctx) error {
|
func (u *DailyChecklistController) GetAllTasks(c *fiber.Ctx) error {
|
||||||
checklistParam := c.Query("checklist_id", "")
|
checklistParam := c.Query("checklist_id", "")
|
||||||
if checklistParam == "" {
|
if checklistParam == "" {
|
||||||
@@ -241,3 +504,21 @@ func (u *DailyChecklistController) GetAllTasks(c *fiber.Ctx) error {
|
|||||||
Data: result,
|
Data: result,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *DailyChecklistController) UpdateAssignment(c *fiber.Ctx) error {
|
||||||
|
req := new(validation.UpdateAssignment)
|
||||||
|
if err := c.BodyParser(req); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := u.DailyChecklistService.UpdateAssignment(c, req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.Success{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Assignment updated successfully",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
employeeDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/employees/dto"
|
||||||
|
kandangDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto"
|
||||||
|
phaseActivityDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phase-activities/dto"
|
||||||
|
phasesDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phasess/dto"
|
||||||
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
|
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -15,15 +19,100 @@ type DailyChecklistRelationDTO struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type DailyChecklistListDTO struct {
|
type DailyChecklistListDTO struct {
|
||||||
Id uint `json:"id"`
|
Id uint `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
CreatedUser *userDTO.UserRelationDTO `json:"created_user"`
|
Status string `json:"status"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
Category string `json:"category"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
Date time.Time `json:"date"`
|
||||||
|
Kandang *kandangDTO.KandangRelationDTO `json:"kandang,omitempty"`
|
||||||
|
CreatedUser *userDTO.UserRelationDTO `json:"created_user"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
TotalPhase int `json:"total_phase"`
|
||||||
|
TotalActivity int `json:"total_activity"`
|
||||||
|
Progress int `json:"progress"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type DailyChecklistDetailDTO struct {
|
type DailyChecklistDetailDTO struct {
|
||||||
DailyChecklistListDTO
|
DailyChecklistListDTO
|
||||||
|
Phases []DailyChecklistPhaseDTO `json:"phases"`
|
||||||
|
Tasks []DailyChecklistActivityTaskDTO `json:"tasks"`
|
||||||
|
AssignedEmployees []employeeDTO.EmployeesRelationDTO `json:"assigned_employees"`
|
||||||
|
TotalActivity int `json:"total_activity"`
|
||||||
|
Progress float64 `json:"progress"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DailyChecklistSummaryDTO struct {
|
||||||
|
EmployeeID uint `json:"employee_id"`
|
||||||
|
EmployeeName string `json:"employee_name"`
|
||||||
|
KandangID uint `json:"kandang_id"`
|
||||||
|
KandangName string `json:"kandang_name"`
|
||||||
|
TotalActivity int `json:"total_activity"`
|
||||||
|
ActivityDone int `json:"activity_done"`
|
||||||
|
ActivityLeft int `json:"activity_left"`
|
||||||
|
CompletionRate int `json:"completion_rate"`
|
||||||
|
LastActivity *time.Time `json:"last_activity,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DailyChecklistPerformanceOverviewDTO struct {
|
||||||
|
EmployeeID uint `json:"employee_id"`
|
||||||
|
EmployeeName string `json:"employee_name"`
|
||||||
|
TotalActivity int `json:"total_activity"`
|
||||||
|
ActivityDone int `json:"activity_done"`
|
||||||
|
ActivityLeft int `json:"activity_left"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DailyChecklistReportDTO struct {
|
||||||
|
Area DailyChecklistReportEntityDTO `json:"area"`
|
||||||
|
Farm DailyChecklistReportEntityDTO `json:"farm"`
|
||||||
|
Kandang DailyChecklistReportEntityDTO `json:"kandang"`
|
||||||
|
ABK DailyChecklistReportEntityDTO `json:"abk"`
|
||||||
|
Phase string `json:"phase"`
|
||||||
|
DailyActivities map[string]int `json:"daily_activities"`
|
||||||
|
Summary DailyChecklistReportSummaryDTO `json:"summary"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DailyChecklistReportEntityDTO struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DailyChecklistReportSummaryDTO struct {
|
||||||
|
TotalChecklist int `json:"total_checklist"`
|
||||||
|
JumlahHariEfektif int `json:"jumlah_hari_efektif"`
|
||||||
|
AbkPercentage int `json:"abk_percentage"`
|
||||||
|
KandangPercentage int `json:"kandang_percentage"`
|
||||||
|
Kategori DailyChecklistReportCategoryDTO `json:"kategori"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DailyChecklistReportCategoryDTO struct {
|
||||||
|
Kurang int `json:"kurang"`
|
||||||
|
Cukup int `json:"cukup"`
|
||||||
|
Baik int `json:"baik"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DailyChecklistPhaseDTO struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
PhaseId uint `json:"phase_id"`
|
||||||
|
Phase phasesDTO.PhasesListDTO `json:"phase"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DailyChecklistActivityTaskDTO struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
ChecklistId uint `json:"checklist_id"`
|
||||||
|
PhaseId uint `json:"phase_id"`
|
||||||
|
PhaseActivityId uint `json:"phase_activity_id"`
|
||||||
|
TimeType *string `json:"time_type"`
|
||||||
|
Notes *string `json:"notes"`
|
||||||
|
Phase phasesDTO.PhasesListDTO `json:"phase"`
|
||||||
|
PhaseActivity phaseActivityDTO.PhaseActivityListDTO `json:"phase_activity"`
|
||||||
|
Assignments []DailyChecklistAssignmentDTO `json:"assignments"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DailyChecklistAssignmentDTO struct {
|
||||||
|
Employee employeeDTO.EmployeesRelationDTO `json:"employee"`
|
||||||
|
Checked bool `json:"checked"`
|
||||||
|
Note *string `json:"note"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Mapper Functions ===
|
// === Mapper Functions ===
|
||||||
@@ -52,25 +141,92 @@ func ToDailyChecklistListDTO(e entity.DailyChecklist) DailyChecklistListDTO {
|
|||||||
name = *e.Name
|
name = *e.Name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var status string
|
||||||
|
if e.Status != nil {
|
||||||
|
status = *e.Status
|
||||||
|
}
|
||||||
|
|
||||||
|
var kandang *kandangDTO.KandangRelationDTO
|
||||||
|
if e.Kandang.Id != 0 {
|
||||||
|
mapped := kandangDTO.ToKandangRelationDTO(e.Kandang)
|
||||||
|
kandang = &mapped
|
||||||
|
}
|
||||||
|
|
||||||
return DailyChecklistListDTO{
|
return DailyChecklistListDTO{
|
||||||
Id: e.Id,
|
Id: e.Id,
|
||||||
Name: name,
|
Name: name,
|
||||||
CreatedAt: e.CreatedAt,
|
Status: status,
|
||||||
UpdatedAt: e.UpdatedAt,
|
Category: e.Category,
|
||||||
CreatedUser: createdUser,
|
Date: e.Date,
|
||||||
|
Kandang: kandang,
|
||||||
|
CreatedAt: e.CreatedAt,
|
||||||
|
UpdatedAt: e.UpdatedAt,
|
||||||
|
CreatedUser: createdUser,
|
||||||
|
TotalPhase: 0,
|
||||||
|
TotalActivity: 0,
|
||||||
|
Progress: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ToDailyChecklistListDTOs(e []entity.DailyChecklist) []DailyChecklistListDTO {
|
func ToDailyChecklistDetailDTO(checklist entity.DailyChecklist, phases []entity.DailyChecklistPhase, tasks []entity.DailyChecklistActivityTask, assignedEmployees []entity.Employee, totalActivities int, progress float64) DailyChecklistDetailDTO {
|
||||||
result := make([]DailyChecklistListDTO, len(e))
|
phaseDTOs := make([]DailyChecklistPhaseDTO, 0, len(phases))
|
||||||
for i, r := range e {
|
for _, phase := range phases {
|
||||||
result[i] = ToDailyChecklistListDTO(r)
|
phaseDTOs = append(phaseDTOs, DailyChecklistPhaseDTO{
|
||||||
|
Id: phase.Id,
|
||||||
|
PhaseId: phase.PhaseId,
|
||||||
|
Phase: phasesDTO.ToPhasesListDTO(phase.Phase),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
taskDTOs := make([]DailyChecklistActivityTaskDTO, 0, len(tasks))
|
||||||
|
for _, task := range tasks {
|
||||||
|
mappedAssignments := make([]DailyChecklistAssignmentDTO, 0, len(task.Assignments))
|
||||||
|
for _, assignment := range task.Assignments {
|
||||||
|
if assignment.Employee.Id == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
mapped := DailyChecklistAssignmentDTO{
|
||||||
|
Employee: employeeDTO.ToEmployeesRelationDTO(assignment.Employee),
|
||||||
|
Checked: assignment.Checked,
|
||||||
|
Note: assignment.Note,
|
||||||
|
}
|
||||||
|
mappedAssignments = append(mappedAssignments, mapped)
|
||||||
|
}
|
||||||
|
|
||||||
|
phaseDTO := phasesDTO.PhasesListDTO{}
|
||||||
|
if task.Phase.Id != 0 {
|
||||||
|
phaseDTO = phasesDTO.ToPhasesListDTO(task.Phase)
|
||||||
|
}
|
||||||
|
|
||||||
|
activityDTO := phaseActivityDTO.PhaseActivityListDTO{}
|
||||||
|
if task.PhaseActivity.Id != 0 {
|
||||||
|
activityDTO = phaseActivityDTO.ToPhaseActivityListDTO(task.PhaseActivity)
|
||||||
|
}
|
||||||
|
|
||||||
|
taskDTOs = append(taskDTOs, DailyChecklistActivityTaskDTO{
|
||||||
|
Id: task.Id,
|
||||||
|
ChecklistId: task.ChecklistId,
|
||||||
|
PhaseId: task.PhaseId,
|
||||||
|
PhaseActivityId: task.PhaseActivityId,
|
||||||
|
TimeType: task.TimeType,
|
||||||
|
Notes: task.Notes,
|
||||||
|
Phase: phaseDTO,
|
||||||
|
PhaseActivity: activityDTO,
|
||||||
|
Assignments: mappedAssignments,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
assignedDTOs := make([]employeeDTO.EmployeesRelationDTO, 0, len(assignedEmployees))
|
||||||
|
for _, emp := range assignedEmployees {
|
||||||
|
assignedDTOs = append(assignedDTOs, employeeDTO.ToEmployeesRelationDTO(emp))
|
||||||
}
|
}
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToDailyChecklistDetailDTO(e entity.DailyChecklist) DailyChecklistDetailDTO {
|
|
||||||
return DailyChecklistDetailDTO{
|
return DailyChecklistDetailDTO{
|
||||||
DailyChecklistListDTO: ToDailyChecklistListDTO(e),
|
DailyChecklistListDTO: ToDailyChecklistListDTO(checklist),
|
||||||
|
Phases: phaseDTOs,
|
||||||
|
Tasks: taskDTOs,
|
||||||
|
AssignedEmployees: assignedDTOs,
|
||||||
|
TotalActivity: totalActivities,
|
||||||
|
Progress: progress,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package dailyChecklists
|
package dailyChecklists
|
||||||
|
|
||||||
import (
|
import (
|
||||||
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||||
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/controllers"
|
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/controllers"
|
||||||
dailyChecklist "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/services"
|
dailyChecklist "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/services"
|
||||||
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||||
@@ -13,23 +13,51 @@ func DailyChecklistRoutes(v1 fiber.Router, u user.UserService, s dailyChecklist.
|
|||||||
ctrl := controller.NewDailyChecklistController(s)
|
ctrl := controller.NewDailyChecklistController(s)
|
||||||
|
|
||||||
route := v1.Group("/daily-checklists")
|
route := v1.Group("/daily-checklists")
|
||||||
route.Use(m.Auth(u))
|
// route.Use(m.Auth(u))
|
||||||
|
|
||||||
route.Get("/", ctrl.GetAll)
|
route.Get("/", ctrl.GetAll)
|
||||||
|
route.Get("/report", ctrl.GetReport)
|
||||||
|
|
||||||
|
route.Get("/summary", ctrl.GetSummary)
|
||||||
|
|
||||||
|
route.Get("/report", ctrl.GetReport)
|
||||||
|
|
||||||
|
// create daily checklist
|
||||||
route.Post("/", ctrl.CreateOne)
|
route.Post("/", ctrl.CreateOne)
|
||||||
|
|
||||||
|
// get detail data daily checklist by id
|
||||||
|
route.Get("/relation/:idDailyChecklist", ctrl.GetOne)
|
||||||
|
|
||||||
|
// get phases by daily checklist id
|
||||||
|
route.Get("/phase/:idDailyChecklist", ctrl.GetPhaseByIdChecklist)
|
||||||
|
|
||||||
// create task
|
// create task
|
||||||
|
/*
|
||||||
|
ketika add phase
|
||||||
|
*/
|
||||||
route.Post("/phase/:idDailyChecklist", ctrl.CreateDailyChecklistPhase)
|
route.Post("/phase/:idDailyChecklist", ctrl.CreateDailyChecklistPhase)
|
||||||
|
|
||||||
// create assigment
|
// create assigment
|
||||||
|
/*
|
||||||
|
ketika add ABK
|
||||||
|
*/
|
||||||
route.Post("/assignment/:idDailyChecklist", ctrl.CreateAssignment)
|
route.Post("/assignment/:idDailyChecklist", ctrl.CreateAssignment)
|
||||||
|
|
||||||
// remove assignment
|
// remove assignment
|
||||||
|
/*
|
||||||
|
ketika remove ABK
|
||||||
|
*/
|
||||||
route.Delete("/:idDailyChecklist/assignments/:idEmployee", ctrl.RemoveAssignment)
|
route.Delete("/:idDailyChecklist/assignments/:idEmployee", ctrl.RemoveAssignment)
|
||||||
|
|
||||||
//get all tasks
|
//get all tasks
|
||||||
route.Get("/tasks", ctrl.GetAllTasks)
|
route.Get("/tasks", ctrl.GetAllTasks)
|
||||||
|
|
||||||
route.Get("/:id", ctrl.GetOne)
|
// update assignment
|
||||||
route.Patch("/:id", ctrl.UpdateOne)
|
/*
|
||||||
route.Delete("/:id", ctrl.DeleteOne)
|
ketika check dan uncheck tugas oleh ABK
|
||||||
|
*/
|
||||||
|
route.Post("/assignment", ctrl.UpdateAssignment)
|
||||||
|
|
||||||
|
route.Patch("/:idDailyChecklist", ctrl.UpdateOne)
|
||||||
|
route.Delete("/:idDailyChecklist", ctrl.DeleteOne)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"math"
|
||||||
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -20,7 +22,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type DailyChecklistService interface {
|
type DailyChecklistService interface {
|
||||||
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.DailyChecklist, int64, error)
|
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]DailyChecklistListItem, int64, error)
|
||||||
GetOne(ctx *fiber.Ctx, id uint) (*entity.DailyChecklist, error)
|
GetOne(ctx *fiber.Ctx, id uint) (*entity.DailyChecklist, error)
|
||||||
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.DailyChecklist, error)
|
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.DailyChecklist, error)
|
||||||
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.DailyChecklist, error)
|
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.DailyChecklist, error)
|
||||||
@@ -29,6 +31,11 @@ type DailyChecklistService interface {
|
|||||||
AssignTasks(ctx *fiber.Ctx, id uint, req *validation.AssignTask) error
|
AssignTasks(ctx *fiber.Ctx, id uint, req *validation.AssignTask) error
|
||||||
RemoveAssignment(ctx *fiber.Ctx, id uint, employeeID uint) error
|
RemoveAssignment(ctx *fiber.Ctx, id uint, employeeID uint) error
|
||||||
GetTasks(ctx *fiber.Ctx, checklistID uint) ([]entity.DailyChecklistActivityTask, error)
|
GetTasks(ctx *fiber.Ctx, checklistID uint) ([]entity.DailyChecklistActivityTask, error)
|
||||||
|
UpdateAssignment(ctx *fiber.Ctx, req *validation.UpdateAssignment) error
|
||||||
|
GetChecklistPhaseIDs(ctx *fiber.Ctx, checklistID uint) ([]uint, error)
|
||||||
|
GetDetail(ctx *fiber.Ctx, id uint) (*DailyChecklistDetail, error)
|
||||||
|
GetSummary(ctx *fiber.Ctx, params *validation.SummaryQuery) ([]DailyChecklistSummary, error)
|
||||||
|
GetReport(ctx *fiber.Ctx, params *validation.ReportQuery) ([]DailyChecklistReportItem, int64, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type dailyChecklistService struct {
|
type dailyChecklistService struct {
|
||||||
@@ -38,6 +45,69 @@ type dailyChecklistService struct {
|
|||||||
PhaseRepo phaseRepo.PhasesRepository
|
PhaseRepo phaseRepo.PhasesRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DailyChecklistDetail struct {
|
||||||
|
Checklist entity.DailyChecklist
|
||||||
|
Phases []entity.DailyChecklistPhase
|
||||||
|
Tasks []entity.DailyChecklistActivityTask
|
||||||
|
AssignedEmployees []entity.Employee
|
||||||
|
TotalActivities int
|
||||||
|
Progress float64
|
||||||
|
}
|
||||||
|
|
||||||
|
type DailyChecklistListItem struct {
|
||||||
|
ID uint
|
||||||
|
Name *string
|
||||||
|
Date time.Time
|
||||||
|
Category string
|
||||||
|
Status *string
|
||||||
|
CreatedAt time.Time
|
||||||
|
UpdatedAt time.Time
|
||||||
|
Kandang entity.Kandang
|
||||||
|
TotalPhase int
|
||||||
|
TotalActivity int
|
||||||
|
Progress int
|
||||||
|
}
|
||||||
|
|
||||||
|
type DailyChecklistSummary struct {
|
||||||
|
EmployeeID uint
|
||||||
|
EmployeeName string
|
||||||
|
KandangID uint
|
||||||
|
KandangName string
|
||||||
|
TotalActivity int
|
||||||
|
ActivityDone int
|
||||||
|
ActivityLeft int
|
||||||
|
CompletionRate int
|
||||||
|
LastActivity *time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type DailyChecklistReportItem struct {
|
||||||
|
AreaID uint
|
||||||
|
AreaName string
|
||||||
|
LocationID uint
|
||||||
|
LocationName string
|
||||||
|
KandangID uint
|
||||||
|
KandangName string
|
||||||
|
EmployeeID uint
|
||||||
|
EmployeeName string
|
||||||
|
PhaseName string
|
||||||
|
DailyActivities map[string]int
|
||||||
|
Summary DailyChecklistReportSummary
|
||||||
|
}
|
||||||
|
|
||||||
|
type DailyChecklistReportSummary struct {
|
||||||
|
TotalChecklist int
|
||||||
|
JumlahHariEfektif int
|
||||||
|
AbkPercentage int
|
||||||
|
KandangPercentage int
|
||||||
|
Category DailyChecklistReportCategory
|
||||||
|
}
|
||||||
|
|
||||||
|
type DailyChecklistReportCategory struct {
|
||||||
|
Kurang int
|
||||||
|
Cukup int
|
||||||
|
Baik int
|
||||||
|
}
|
||||||
|
|
||||||
func NewDailyChecklistService(repo repository.DailyChecklistRepository, phaseRepo phaseRepo.PhasesRepository, validate *validator.Validate) DailyChecklistService {
|
func NewDailyChecklistService(repo repository.DailyChecklistRepository, phaseRepo phaseRepo.PhasesRepository, validate *validator.Validate) DailyChecklistService {
|
||||||
return &dailyChecklistService{
|
return &dailyChecklistService{
|
||||||
Log: utils.Log,
|
Log: utils.Log,
|
||||||
@@ -48,29 +118,163 @@ func NewDailyChecklistService(repo repository.DailyChecklistRepository, phaseRep
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s dailyChecklistService) withRelations(db *gorm.DB) *gorm.DB {
|
func (s dailyChecklistService) withRelations(db *gorm.DB) *gorm.DB {
|
||||||
return db
|
return db.Preload("Kandang")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s dailyChecklistService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.DailyChecklist, int64, error) {
|
func (s dailyChecklistService) GetAll(c *fiber.Ctx, params *validation.Query) ([]DailyChecklistListItem, int64, error) {
|
||||||
if err := s.Validate.Struct(params); err != nil {
|
if err := s.Validate.Struct(params); err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
offset := (params.Page - 1) * params.Limit
|
offset := (params.Page - 1) * params.Limit
|
||||||
|
|
||||||
dailyChecklists, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
db := s.Repository.DB().WithContext(c.Context()).
|
||||||
db = s.withRelations(db)
|
Table("daily_checklists dc").
|
||||||
if params.Search != "" {
|
Joins("JOIN kandangs k ON k.id = dc.kandang_id")
|
||||||
return db.Where("name LIKE ?", "%"+params.Search+"%")
|
|
||||||
}
|
|
||||||
return db.Order("created_at DESC").Order("updated_at DESC")
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
if params.DateFrom != "" {
|
||||||
|
dateFrom, err := time.Parse("2006-01-02", params.DateFrom)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, fiber.NewError(fiber.StatusBadRequest, "invalid date_from format, use YYYY-MM-DD")
|
||||||
|
}
|
||||||
|
db = db.Where("dc.date >= ?", dateFrom)
|
||||||
|
}
|
||||||
|
|
||||||
|
if params.DateTo != "" {
|
||||||
|
dateTo, err := time.Parse("2006-01-02", params.DateTo)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, fiber.NewError(fiber.StatusBadRequest, "invalid date_to format, use YYYY-MM-DD")
|
||||||
|
}
|
||||||
|
db = db.Where("dc.date <= ?", dateTo)
|
||||||
|
}
|
||||||
|
|
||||||
|
if params.KandangID != nil {
|
||||||
|
db = db.Where("dc.kandang_id = ?", *params.KandangID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if params.Status != "" {
|
||||||
|
db = db.Where("dc.status = ?", params.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
if params.Search != "" {
|
||||||
|
like := "%" + params.Search + "%"
|
||||||
|
db = db.Where("(k.name ILIKE ? OR dc.category ILIKE ?)", like, like)
|
||||||
|
}
|
||||||
|
|
||||||
|
countDB := db.Session(&gorm.Session{})
|
||||||
|
var total int64
|
||||||
|
if err := countDB.Count(&total).Error; err != nil {
|
||||||
|
s.Log.Errorf("Failed to count dailyChecklists: %+v", err)
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
type dailyChecklistListRow struct {
|
||||||
|
ID uint
|
||||||
|
Name *string
|
||||||
|
Date time.Time
|
||||||
|
Category string
|
||||||
|
Status *string
|
||||||
|
CreatedAt time.Time
|
||||||
|
UpdatedAt time.Time
|
||||||
|
KandangID uint
|
||||||
|
TotalPhase int64
|
||||||
|
TotalActivity int64
|
||||||
|
TotalAssignments int64
|
||||||
|
CompletedAssignments int64
|
||||||
|
}
|
||||||
|
|
||||||
|
rows := make([]dailyChecklistListRow, 0)
|
||||||
|
selectDB := db.Session(&gorm.Session{})
|
||||||
|
if err := selectDB.
|
||||||
|
Select(`
|
||||||
|
dc.id,
|
||||||
|
dc.name,
|
||||||
|
dc.date,
|
||||||
|
dc.category,
|
||||||
|
dc.status,
|
||||||
|
dc.created_at,
|
||||||
|
dc.updated_at,
|
||||||
|
dc.kandang_id,
|
||||||
|
COALESCE((
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM daily_checklist_phases dcp
|
||||||
|
WHERE dcp.checklist_id = dc.id
|
||||||
|
), 0) AS total_phase,
|
||||||
|
COALESCE((
|
||||||
|
SELECT COUNT(pa.id)
|
||||||
|
FROM daily_checklist_phases dcp
|
||||||
|
JOIN phase_activities pa ON pa.phase_id = dcp.phase_id
|
||||||
|
WHERE dcp.checklist_id = dc.id AND pa.deleted_at IS NULL
|
||||||
|
), 0) AS total_activity,
|
||||||
|
COALESCE((
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM daily_checklist_activity_task_assignments dca
|
||||||
|
JOIN daily_checklist_activity_tasks dcat ON dcat.id = dca.task_id
|
||||||
|
WHERE dcat.checklist_id = dc.id
|
||||||
|
), 0) AS total_assignments,
|
||||||
|
COALESCE((
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM daily_checklist_activity_task_assignments dca
|
||||||
|
JOIN daily_checklist_activity_tasks dcat ON dcat.id = dca.task_id
|
||||||
|
WHERE dcat.checklist_id = dc.id AND dca.checked
|
||||||
|
), 0) AS completed_assignments`).
|
||||||
|
Order("dc.date DESC, dc.created_at DESC").
|
||||||
|
Offset(offset).
|
||||||
|
Limit(params.Limit).
|
||||||
|
Scan(&rows).Error; err != nil {
|
||||||
s.Log.Errorf("Failed to get dailyChecklists: %+v", err)
|
s.Log.Errorf("Failed to get dailyChecklists: %+v", err)
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
return dailyChecklists, total, nil
|
|
||||||
|
kandangIDs := make([]uint, 0, len(rows))
|
||||||
|
seen := make(map[uint]struct{})
|
||||||
|
for _, row := range rows {
|
||||||
|
if _, ok := seen[row.KandangID]; !ok {
|
||||||
|
seen[row.KandangID] = struct{}{}
|
||||||
|
kandangIDs = append(kandangIDs, row.KandangID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
kandangMap := make(map[uint]entity.Kandang)
|
||||||
|
if len(kandangIDs) > 0 {
|
||||||
|
var kandangs []entity.Kandang
|
||||||
|
if err := s.Repository.DB().WithContext(c.Context()).
|
||||||
|
Where("id IN ?", kandangIDs).
|
||||||
|
Preload("Location").
|
||||||
|
Preload("Pic").
|
||||||
|
Preload("CreatedUser").
|
||||||
|
Find(&kandangs).Error; err != nil {
|
||||||
|
s.Log.Errorf("Failed to get kandangs for daily checklist list: %+v", err)
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
for _, kandang := range kandangs {
|
||||||
|
kandangMap[kandang.Id] = kandang
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
items := make([]DailyChecklistListItem, len(rows))
|
||||||
|
for i, row := range rows {
|
||||||
|
progress := 0
|
||||||
|
if row.TotalAssignments > 0 {
|
||||||
|
progress = int(math.Round(float64(row.CompletedAssignments) / float64(row.TotalAssignments) * 100))
|
||||||
|
}
|
||||||
|
|
||||||
|
items[i] = DailyChecklistListItem{
|
||||||
|
ID: row.ID,
|
||||||
|
Name: row.Name,
|
||||||
|
Date: row.Date,
|
||||||
|
Category: row.Category,
|
||||||
|
Status: row.Status,
|
||||||
|
CreatedAt: row.CreatedAt,
|
||||||
|
UpdatedAt: row.UpdatedAt,
|
||||||
|
Kandang: kandangMap[row.KandangID],
|
||||||
|
TotalPhase: int(row.TotalPhase),
|
||||||
|
TotalActivity: int(row.TotalActivity),
|
||||||
|
Progress: progress,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return items, total, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s dailyChecklistService) GetOne(c *fiber.Ctx, id uint) (*entity.DailyChecklist, error) {
|
func (s dailyChecklistService) GetOne(c *fiber.Ctx, id uint) (*entity.DailyChecklist, error) {
|
||||||
@@ -85,6 +289,72 @@ func (s dailyChecklistService) GetOne(c *fiber.Ctx, id uint) (*entity.DailyCheck
|
|||||||
return dailyChecklist, nil
|
return dailyChecklist, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s dailyChecklistService) GetDetail(c *fiber.Ctx, id uint) (*DailyChecklistDetail, error) {
|
||||||
|
checklist, err := s.GetOne(c, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
db := s.Repository.DB().WithContext(c.Context())
|
||||||
|
|
||||||
|
var phases []entity.DailyChecklistPhase
|
||||||
|
if err := db.
|
||||||
|
Where("checklist_id = ?", id).
|
||||||
|
Preload("Phase", func(tx *gorm.DB) *gorm.DB {
|
||||||
|
return tx.Preload("Activities")
|
||||||
|
}).
|
||||||
|
Order("created_at ASC").
|
||||||
|
Find(&phases).Error; err != nil {
|
||||||
|
s.Log.Errorf("Failed to get phases for daily checklist %d: %+v", id, err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var tasks []entity.DailyChecklistActivityTask
|
||||||
|
if err := db.
|
||||||
|
Where("checklist_id = ?", id).
|
||||||
|
Preload("Phase").
|
||||||
|
Preload("PhaseActivity").
|
||||||
|
Preload("Assignments", func(tx *gorm.DB) *gorm.DB {
|
||||||
|
return tx.Preload("Employee")
|
||||||
|
}).
|
||||||
|
Order("created_at ASC").
|
||||||
|
Find(&tasks).Error; err != nil {
|
||||||
|
s.Log.Errorf("Failed to get tasks for daily checklist %d: %+v", id, err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
assignedEmployees := collectAssignedEmployees(tasks)
|
||||||
|
|
||||||
|
totalActivities := 0
|
||||||
|
for _, phase := range phases {
|
||||||
|
totalActivities += len(phase.Phase.Activities)
|
||||||
|
}
|
||||||
|
|
||||||
|
var totalAssignments, completedAssignments int
|
||||||
|
for _, task := range tasks {
|
||||||
|
for _, assignment := range task.Assignments {
|
||||||
|
totalAssignments++
|
||||||
|
if assignment.Checked {
|
||||||
|
completedAssignments++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var progress float64
|
||||||
|
if totalAssignments > 0 {
|
||||||
|
progress = math.Round((float64(completedAssignments) / float64(totalAssignments)) * 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &DailyChecklistDetail{
|
||||||
|
Checklist: *checklist,
|
||||||
|
Phases: phases,
|
||||||
|
Tasks: tasks,
|
||||||
|
AssignedEmployees: assignedEmployees,
|
||||||
|
TotalActivities: totalActivities,
|
||||||
|
Progress: progress,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *dailyChecklistService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.DailyChecklist, error) {
|
func (s *dailyChecklistService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.DailyChecklist, error) {
|
||||||
if err := s.Validate.Struct(req); err != nil {
|
if err := s.Validate.Struct(req); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -122,14 +392,12 @@ func (s dailyChecklistService) UpdateOne(c *fiber.Ctx, req *validation.Update, i
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
updateBody := make(map[string]any)
|
updateBody := map[string]any{
|
||||||
|
"status": req.Status,
|
||||||
if req.Name != nil {
|
|
||||||
updateBody["name"] = *req.Name
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(updateBody) == 0 {
|
if req.RejectReason != nil {
|
||||||
return s.GetOne(c, id)
|
updateBody["reject_reason"] = *req.RejectReason
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil {
|
if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil {
|
||||||
@@ -296,6 +564,71 @@ func (s dailyChecklistService) GetTasks(c *fiber.Ctx, checklistID uint) ([]entit
|
|||||||
return tasks, nil
|
return tasks, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s dailyChecklistService) GetChecklistPhaseIDs(c *fiber.Ctx, checklistID uint) ([]uint, error) {
|
||||||
|
if checklistID == 0 {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "checklist_id is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := s.Repository.GetByID(c.Context(), checklistID, nil); err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, fiber.NewError(fiber.StatusNotFound, "DailyChecklist not found")
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var phases []entity.DailyChecklistPhase
|
||||||
|
if err := s.Repository.DB().WithContext(c.Context()).
|
||||||
|
Where("checklist_id = ?", checklistID).
|
||||||
|
Order("created_at ASC").
|
||||||
|
Find(&phases).Error; err != nil {
|
||||||
|
s.Log.Errorf("Failed to get daily checklist phases: %+v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
phaseIDs := make([]uint, len(phases))
|
||||||
|
for i, p := range phases {
|
||||||
|
phaseIDs[i] = p.PhaseId
|
||||||
|
}
|
||||||
|
|
||||||
|
return phaseIDs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s dailyChecklistService) UpdateAssignment(c *fiber.Ctx, req *validation.UpdateAssignment) error {
|
||||||
|
if err := s.Validate.Struct(req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
task := new(entity.DailyChecklistActivityTask)
|
||||||
|
if err := s.Repository.DB().WithContext(c.Context()).First(task, req.TaskID).Error; err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return fiber.NewError(fiber.StatusNotFound, "Task not found")
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.EmployeeID == 0 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid employee id")
|
||||||
|
}
|
||||||
|
|
||||||
|
updates := map[string]any{"updated_at": time.Now()}
|
||||||
|
if req.Checked != nil {
|
||||||
|
updates["checked"] = *req.Checked
|
||||||
|
}
|
||||||
|
if req.Note != nil {
|
||||||
|
updates["note"] = *req.Note
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.Repository.DB().WithContext(c.Context()).Clauses(clause.OnConflict{
|
||||||
|
Columns: []clause.Column{{Name: "task_id"}, {Name: "employee_id"}},
|
||||||
|
DoUpdates: clause.Assignments(updates),
|
||||||
|
}).Create(&entity.DailyChecklistActivityTaskAssignment{
|
||||||
|
TaskId: req.TaskID,
|
||||||
|
EmployeeId: req.EmployeeID,
|
||||||
|
Checked: req.Checked != nil && *req.Checked,
|
||||||
|
Note: req.Note,
|
||||||
|
}).Error
|
||||||
|
}
|
||||||
|
|
||||||
func parsePhaseIDs(raw string) ([]uint, error) {
|
func parsePhaseIDs(raw string) ([]uint, error) {
|
||||||
parts := strings.Split(raw, ",")
|
parts := strings.Split(raw, ",")
|
||||||
result := make([]uint, 0, len(parts))
|
result := make([]uint, 0, len(parts))
|
||||||
@@ -355,6 +688,32 @@ func collectTaskIDs(tasks []entity.DailyChecklistActivityTask) []uint {
|
|||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func collectAssignedEmployees(tasks []entity.DailyChecklistActivityTask) []entity.Employee {
|
||||||
|
employeeMap := make(map[uint]entity.Employee)
|
||||||
|
for _, task := range tasks {
|
||||||
|
for _, assignment := range task.Assignments {
|
||||||
|
if assignment.Employee.Id == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, exists := employeeMap[assignment.Employee.Id]; exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
employeeMap[assignment.Employee.Id] = assignment.Employee
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
employees := make([]entity.Employee, 0, len(employeeMap))
|
||||||
|
for _, emp := range employeeMap {
|
||||||
|
employees = append(employees, emp)
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(employees, func(i, j int) bool {
|
||||||
|
return employees[i].Id < employees[j].Id
|
||||||
|
})
|
||||||
|
|
||||||
|
return employees
|
||||||
|
}
|
||||||
func (s dailyChecklistService) AssignTasks(c *fiber.Ctx, id uint, req *validation.AssignTask) error {
|
func (s dailyChecklistService) AssignTasks(c *fiber.Ctx, id uint, req *validation.AssignTask) error {
|
||||||
if err := s.Validate.Struct(req); err != nil {
|
if err := s.Validate.Struct(req); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -408,3 +767,413 @@ func (s dailyChecklistService) AssignTasks(c *fiber.Ctx, id uint, req *validatio
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s dailyChecklistService) GetSummary(c *fiber.Ctx, params *validation.SummaryQuery) ([]DailyChecklistSummary, error) {
|
||||||
|
if err := s.Validate.Struct(params); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
dateFrom, err := time.Parse("2006-01-02", params.DateFrom)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "invalid date_from format, use YYYY-MM-DD")
|
||||||
|
}
|
||||||
|
|
||||||
|
dateTo, err := time.Parse("2006-01-02", params.DateTo)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "invalid date_to format, use YYYY-MM-DD")
|
||||||
|
}
|
||||||
|
|
||||||
|
type summaryRow struct {
|
||||||
|
EmployeeID uint
|
||||||
|
EmployeeName string
|
||||||
|
KandangID uint
|
||||||
|
KandangName string
|
||||||
|
TotalActivity int64
|
||||||
|
ActivityDone int64
|
||||||
|
ActivityLeft int64
|
||||||
|
LastActivity *time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
rows := make([]summaryRow, 0)
|
||||||
|
db := s.Repository.DB().WithContext(c.Context()).
|
||||||
|
Table("daily_checklist_activity_task_assignments AS a").
|
||||||
|
Select(`
|
||||||
|
a.employee_id,
|
||||||
|
e.name AS employee_name,
|
||||||
|
d.kandang_id,
|
||||||
|
k.name AS kandang_name,
|
||||||
|
COUNT(*) AS total_activity,
|
||||||
|
SUM(CASE WHEN a.checked THEN 1 ELSE 0 END) AS activity_done,
|
||||||
|
SUM(CASE WHEN NOT a.checked THEN 1 ELSE 0 END) AS activity_left,
|
||||||
|
MAX(a.updated_at) AS last_activity`).
|
||||||
|
Joins("JOIN daily_checklist_activity_tasks t ON t.id = a.task_id").
|
||||||
|
Joins("JOIN daily_checklists d ON d.id = t.checklist_id").
|
||||||
|
Joins("JOIN kandangs k ON k.id = d.kandang_id").
|
||||||
|
Joins("JOIN employees e ON e.id = a.employee_id").
|
||||||
|
Where("d.date BETWEEN ? AND ? AND d.status = ?", dateFrom, dateTo, "APPROVED")
|
||||||
|
|
||||||
|
if params.Category != "" {
|
||||||
|
db = db.Where("d.category = ?", params.Category)
|
||||||
|
}
|
||||||
|
|
||||||
|
if params.KandangID != nil {
|
||||||
|
db = db.Where("d.kandang_id = ?", *params.KandangID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.
|
||||||
|
Group("a.employee_id, e.name, d.kandang_id, k.name").
|
||||||
|
Order("e.name ASC").
|
||||||
|
Find(&rows).Error; err != nil {
|
||||||
|
s.Log.Errorf("Failed to get daily checklist summary: %+v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
summaries := make([]DailyChecklistSummary, len(rows))
|
||||||
|
for i, row := range rows {
|
||||||
|
completionRate := 0
|
||||||
|
if row.TotalActivity > 0 {
|
||||||
|
completionRate = int(math.Round(float64(row.ActivityDone) / float64(row.TotalActivity) * 100))
|
||||||
|
}
|
||||||
|
|
||||||
|
summaries[i] = DailyChecklistSummary{
|
||||||
|
EmployeeID: row.EmployeeID,
|
||||||
|
EmployeeName: row.EmployeeName,
|
||||||
|
KandangID: row.KandangID,
|
||||||
|
KandangName: row.KandangName,
|
||||||
|
TotalActivity: int(row.TotalActivity),
|
||||||
|
ActivityDone: int(row.ActivityDone),
|
||||||
|
ActivityLeft: int(row.ActivityLeft),
|
||||||
|
CompletionRate: completionRate,
|
||||||
|
LastActivity: row.LastActivity,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return summaries, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s dailyChecklistService) GetReport(c *fiber.Ctx, params *validation.ReportQuery) ([]DailyChecklistReportItem, int64, error) {
|
||||||
|
if err := s.Validate.Struct(params); err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
offset := (params.Page - 1) * params.Limit
|
||||||
|
|
||||||
|
buildBase := func() *gorm.DB {
|
||||||
|
db := s.Repository.DB().WithContext(c.Context()).
|
||||||
|
Table("daily_checklist_activity_task_assignments AS dca").
|
||||||
|
Joins("JOIN daily_checklist_activity_tasks dcat ON dcat.id = dca.task_id").
|
||||||
|
Joins("JOIN daily_checklists dc ON dc.id = dcat.checklist_id").
|
||||||
|
Joins("JOIN employees e ON e.id = dca.employee_id").
|
||||||
|
Joins("JOIN kandangs k ON k.id = dc.kandang_id").
|
||||||
|
Joins("JOIN locations loc ON loc.id = k.location_id").
|
||||||
|
Joins("JOIN areas a ON a.id = loc.area_id").
|
||||||
|
Joins("JOIN phases p ON p.id = dcat.phase_id").
|
||||||
|
Where("EXTRACT(MONTH FROM dc.date) = ?", params.Month).
|
||||||
|
Where("EXTRACT(YEAR FROM dc.date) = ?", params.Year)
|
||||||
|
|
||||||
|
if params.AreaID != nil {
|
||||||
|
db = db.Where("a.id = ?", *params.AreaID)
|
||||||
|
}
|
||||||
|
if params.LocationID != nil {
|
||||||
|
db = db.Where("loc.id = ?", *params.LocationID)
|
||||||
|
}
|
||||||
|
if params.KandangID != nil {
|
||||||
|
db = db.Where("k.id = ?", *params.KandangID)
|
||||||
|
}
|
||||||
|
if params.EmployeeID != nil {
|
||||||
|
db = db.Where("dca.employee_id = ?", *params.EmployeeID)
|
||||||
|
}
|
||||||
|
if params.PhaseID != nil {
|
||||||
|
db = db.Where("p.id = ?", *params.PhaseID)
|
||||||
|
}
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
|
||||||
|
buildGroupedQuery := func() *gorm.DB {
|
||||||
|
return buildBase().
|
||||||
|
Select(`
|
||||||
|
a.id AS area_id,
|
||||||
|
a.name AS area_name,
|
||||||
|
loc.id AS location_id,
|
||||||
|
loc.name AS location_name,
|
||||||
|
k.id AS kandang_id,
|
||||||
|
k.name AS kandang_name,
|
||||||
|
e.id AS employee_id,
|
||||||
|
e.name AS employee_name,
|
||||||
|
p.id AS phase_id,
|
||||||
|
p.name AS phase_name,
|
||||||
|
SUM(CASE WHEN dca.checked THEN 1 ELSE 0 END) AS completed_assignments,
|
||||||
|
COUNT(*) AS total_assignments`).
|
||||||
|
Group("a.id, a.name, loc.id, loc.name, k.id, k.name, e.id, e.name, p.id, p.name")
|
||||||
|
}
|
||||||
|
|
||||||
|
var total int64
|
||||||
|
groupedForCount := buildGroupedQuery()
|
||||||
|
if err := s.Repository.DB().WithContext(c.Context()).
|
||||||
|
Table("(?) AS grouped", groupedForCount).
|
||||||
|
Count(&total).Error; err != nil {
|
||||||
|
s.Log.Errorf("Failed to count report data: %+v", err)
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
type reportRow struct {
|
||||||
|
AreaID uint
|
||||||
|
AreaName string
|
||||||
|
LocationID uint
|
||||||
|
LocationName string
|
||||||
|
KandangID uint
|
||||||
|
KandangName string
|
||||||
|
EmployeeID uint
|
||||||
|
EmployeeName string
|
||||||
|
PhaseID uint
|
||||||
|
PhaseName string
|
||||||
|
CompletedAssignments int64
|
||||||
|
TotalAssignments int64
|
||||||
|
}
|
||||||
|
|
||||||
|
rows := make([]reportRow, 0)
|
||||||
|
if err := buildGroupedQuery().
|
||||||
|
Order("a.name, loc.name, k.name, e.name").
|
||||||
|
Offset(offset).
|
||||||
|
Limit(params.Limit).
|
||||||
|
Scan(&rows).Error; err != nil {
|
||||||
|
s.Log.Errorf("Failed to fetch report data: %+v", err)
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(rows) == 0 {
|
||||||
|
return []DailyChecklistReportItem{}, total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type comboKey struct {
|
||||||
|
EmployeeID uint
|
||||||
|
KandangID uint
|
||||||
|
PhaseID uint
|
||||||
|
}
|
||||||
|
|
||||||
|
type dailyActivityStat struct {
|
||||||
|
Completed int
|
||||||
|
Total int
|
||||||
|
Date time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
employeeIDs := make([]uint, 0)
|
||||||
|
kandangIDs := make([]uint, 0)
|
||||||
|
phaseIDs := make([]uint, 0)
|
||||||
|
comboSet := make(map[comboKey]struct{})
|
||||||
|
employeeSet := make(map[uint]struct{})
|
||||||
|
kandangSet := make(map[uint]struct{})
|
||||||
|
phaseSet := make(map[uint]struct{})
|
||||||
|
|
||||||
|
for _, row := range rows {
|
||||||
|
key := comboKey{EmployeeID: row.EmployeeID, KandangID: row.KandangID, PhaseID: row.PhaseID}
|
||||||
|
comboSet[key] = struct{}{}
|
||||||
|
if _, ok := employeeSet[row.EmployeeID]; !ok {
|
||||||
|
employeeSet[row.EmployeeID] = struct{}{}
|
||||||
|
employeeIDs = append(employeeIDs, row.EmployeeID)
|
||||||
|
}
|
||||||
|
if _, ok := kandangSet[row.KandangID]; !ok {
|
||||||
|
kandangSet[row.KandangID] = struct{}{}
|
||||||
|
kandangIDs = append(kandangIDs, row.KandangID)
|
||||||
|
}
|
||||||
|
if _, ok := phaseSet[row.PhaseID]; !ok {
|
||||||
|
phaseSet[row.PhaseID] = struct{}{}
|
||||||
|
phaseIDs = append(phaseIDs, row.PhaseID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dailyActivityMap := make(map[comboKey]map[string]dailyActivityStat)
|
||||||
|
if len(employeeIDs) > 0 {
|
||||||
|
var dailyRows []struct {
|
||||||
|
EmployeeID uint
|
||||||
|
KandangID uint
|
||||||
|
PhaseID uint
|
||||||
|
Date time.Time
|
||||||
|
Completed int64
|
||||||
|
Total int64
|
||||||
|
}
|
||||||
|
|
||||||
|
dailyQuery := buildBase().
|
||||||
|
Where("dca.employee_id IN ?", employeeIDs).
|
||||||
|
Where("dc.kandang_id IN ?", kandangIDs).
|
||||||
|
Where("dcat.phase_id IN ?", phaseIDs).
|
||||||
|
Select(`
|
||||||
|
dca.employee_id,
|
||||||
|
dc.kandang_id,
|
||||||
|
dcat.phase_id,
|
||||||
|
dc.date,
|
||||||
|
SUM(CASE WHEN dca.checked THEN 1 ELSE 0 END) AS completed,
|
||||||
|
COUNT(*) AS total`).
|
||||||
|
Group("dca.employee_id, dc.kandang_id, dcat.phase_id, dc.date")
|
||||||
|
|
||||||
|
if err := dailyQuery.Scan(&dailyRows).Error; err != nil {
|
||||||
|
s.Log.Errorf("Failed to fetch daily activities for report: %+v", err)
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, row := range dailyRows {
|
||||||
|
key := comboKey{EmployeeID: row.EmployeeID, KandangID: row.KandangID, PhaseID: row.PhaseID}
|
||||||
|
if _, ok := comboSet[key]; !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := dailyActivityMap[key]; !ok {
|
||||||
|
dailyActivityMap[key] = make(map[string]dailyActivityStat)
|
||||||
|
}
|
||||||
|
day := strconv.Itoa(row.Date.Day())
|
||||||
|
dailyActivityMap[key][day] = dailyActivityStat{
|
||||||
|
Completed: int(row.Completed),
|
||||||
|
Total: int(row.Total),
|
||||||
|
Date: row.Date,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
employeeStats := make(map[uint]struct {
|
||||||
|
Completed int64
|
||||||
|
Total int64
|
||||||
|
})
|
||||||
|
var employeeRows []struct {
|
||||||
|
EmployeeID uint
|
||||||
|
Completed int64
|
||||||
|
Total int64
|
||||||
|
}
|
||||||
|
if err := buildBase().
|
||||||
|
Select(`
|
||||||
|
dca.employee_id,
|
||||||
|
SUM(CASE WHEN dca.checked THEN 1 ELSE 0 END) AS completed,
|
||||||
|
COUNT(*) AS total`).
|
||||||
|
Group("dca.employee_id").
|
||||||
|
Scan(&employeeRows).Error; err != nil {
|
||||||
|
s.Log.Errorf("Failed to fetch employee stats for report: %+v", err)
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
for _, row := range employeeRows {
|
||||||
|
employeeStats[row.EmployeeID] = struct {
|
||||||
|
Completed int64
|
||||||
|
Total int64
|
||||||
|
}{Completed: row.Completed, Total: row.Total}
|
||||||
|
}
|
||||||
|
|
||||||
|
kandangStats := make(map[uint]struct {
|
||||||
|
Completed int64
|
||||||
|
Total int64
|
||||||
|
})
|
||||||
|
var kandangRows []struct {
|
||||||
|
KandangID uint
|
||||||
|
Completed int64
|
||||||
|
Total int64
|
||||||
|
}
|
||||||
|
if err := buildBase().
|
||||||
|
Select(`
|
||||||
|
dc.kandang_id,
|
||||||
|
SUM(CASE WHEN dca.checked THEN 1 ELSE 0 END) AS completed,
|
||||||
|
COUNT(*) AS total`).
|
||||||
|
Group("dc.kandang_id").
|
||||||
|
Scan(&kandangRows).Error; err != nil {
|
||||||
|
s.Log.Errorf("Failed to fetch kandang stats for report: %+v", err)
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
for _, row := range kandangRows {
|
||||||
|
kandangStats[row.KandangID] = struct {
|
||||||
|
Completed int64
|
||||||
|
Total int64
|
||||||
|
}{Completed: row.Completed, Total: row.Total}
|
||||||
|
}
|
||||||
|
|
||||||
|
var configs []entity.ConfigChecklist
|
||||||
|
if err := s.Repository.DB().WithContext(c.Context()).
|
||||||
|
Order("date ASC").
|
||||||
|
Find(&configs).Error; err != nil {
|
||||||
|
s.Log.Errorf("Failed to load config checklists: %+v", err)
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
getConfigForDate := func(date time.Time) *entity.ConfigChecklist {
|
||||||
|
var selected *entity.ConfigChecklist
|
||||||
|
for i := range configs {
|
||||||
|
if !configs[i].Date.After(date) {
|
||||||
|
selected = &configs[i]
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if selected == nil {
|
||||||
|
return &entity.ConfigChecklist{
|
||||||
|
PercentageThresholdBad: 50,
|
||||||
|
PercentageThresholdEnough: 75,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return selected
|
||||||
|
}
|
||||||
|
|
||||||
|
items := make([]DailyChecklistReportItem, len(rows))
|
||||||
|
for i, row := range rows {
|
||||||
|
key := comboKey{EmployeeID: row.EmployeeID, KandangID: row.KandangID, PhaseID: row.PhaseID}
|
||||||
|
|
||||||
|
activities := dailyActivityMap[key]
|
||||||
|
if activities == nil {
|
||||||
|
activities = map[string]dailyActivityStat{}
|
||||||
|
}
|
||||||
|
|
||||||
|
totalChecklist := 0
|
||||||
|
categoryCounts := DailyChecklistReportCategory{}
|
||||||
|
activityOutput := make(map[string]int, len(activities))
|
||||||
|
|
||||||
|
for day, stat := range activities {
|
||||||
|
activityOutput[day] = stat.Completed
|
||||||
|
totalChecklist += stat.Completed
|
||||||
|
|
||||||
|
if stat.Total == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := getConfigForDate(stat.Date)
|
||||||
|
if cfg == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
progress := int(math.Ceil(float64(stat.Completed) / float64(stat.Total) * 100))
|
||||||
|
if progress <= cfg.PercentageThresholdBad {
|
||||||
|
categoryCounts.Kurang++
|
||||||
|
} else if progress <= cfg.PercentageThresholdEnough {
|
||||||
|
categoryCounts.Cukup++
|
||||||
|
} else {
|
||||||
|
categoryCounts.Baik++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
employeeStat := employeeStats[row.EmployeeID]
|
||||||
|
abkPercentage := 0
|
||||||
|
if employeeStat.Total > 0 {
|
||||||
|
abkPercentage = int(math.Round(float64(employeeStat.Completed) / float64(employeeStat.Total) * 100))
|
||||||
|
}
|
||||||
|
|
||||||
|
kandangStat := kandangStats[row.KandangID]
|
||||||
|
kandangPercentage := 0
|
||||||
|
if kandangStat.Total > 0 {
|
||||||
|
kandangPercentage = int(math.Round(float64(kandangStat.Completed) / float64(kandangStat.Total) * 100))
|
||||||
|
}
|
||||||
|
|
||||||
|
items[i] = DailyChecklistReportItem{
|
||||||
|
AreaID: row.AreaID,
|
||||||
|
AreaName: row.AreaName,
|
||||||
|
LocationID: row.LocationID,
|
||||||
|
LocationName: row.LocationName,
|
||||||
|
KandangID: row.KandangID,
|
||||||
|
KandangName: row.KandangName,
|
||||||
|
EmployeeID: row.EmployeeID,
|
||||||
|
EmployeeName: row.EmployeeName,
|
||||||
|
PhaseName: row.PhaseName,
|
||||||
|
DailyActivities: activityOutput,
|
||||||
|
Summary: DailyChecklistReportSummary{
|
||||||
|
TotalChecklist: totalChecklist,
|
||||||
|
JumlahHariEfektif: len(activities),
|
||||||
|
AbkPercentage: abkPercentage,
|
||||||
|
KandangPercentage: kandangPercentage,
|
||||||
|
Category: categoryCounts,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return items, total, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,13 +8,18 @@ type Create struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Update struct {
|
type Update struct {
|
||||||
Name *string `json:"name,omitempty" validate:"omitempty"`
|
Status string `json:"status" validate:"required"`
|
||||||
|
RejectReason *string `json:"reject_reason" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Query struct {
|
type Query struct {
|
||||||
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
|
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
|
||||||
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
|
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
|
||||||
Search string `query:"search" validate:"omitempty,max=50"`
|
Search string `query:"search" validate:"omitempty,max=50"`
|
||||||
|
DateFrom string `query:"date_from" validate:"omitempty"`
|
||||||
|
DateTo string `query:"date_to" validate:"omitempty"`
|
||||||
|
Status string `query:"status" validate:"omitempty"`
|
||||||
|
KandangID *uint `query:"kandang_id" validate:"omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AssignPhases struct {
|
type AssignPhases struct {
|
||||||
@@ -24,3 +29,29 @@ type AssignPhases struct {
|
|||||||
type AssignTask struct {
|
type AssignTask struct {
|
||||||
EmployeeIDs string `json:"employee_ids" validate:"required"`
|
EmployeeIDs string `json:"employee_ids" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type UpdateAssignment struct {
|
||||||
|
TaskID uint `json:"task_id" validate:"required"`
|
||||||
|
EmployeeID uint `json:"employee_id" validate:"required"`
|
||||||
|
Checked *bool `json:"checked,omitempty"`
|
||||||
|
Note *string `json:"note,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SummaryQuery struct {
|
||||||
|
DateFrom string `query:"date_from" validate:"required"`
|
||||||
|
DateTo string `query:"date_to" validate:"required"`
|
||||||
|
Category string `query:"category" validate:"omitempty"`
|
||||||
|
KandangID *uint `query:"kandang_id" validate:"omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ReportQuery struct {
|
||||||
|
Page int `query:"page" validate:"required,number,min=1,gt=0"`
|
||||||
|
Limit int `query:"limit" validate:"required,number,min=1,max=100,gt=0"`
|
||||||
|
Month int `query:"bulan" validate:"required,number,min=1,max=12"`
|
||||||
|
Year int `query:"tahun" validate:"required,number,min=1900"`
|
||||||
|
AreaID *uint `query:"area_id" validate:"omitempty"`
|
||||||
|
LocationID *uint `query:"location_id" validate:"omitempty"`
|
||||||
|
KandangID *uint `query:"kandang_id" validate:"omitempty"`
|
||||||
|
EmployeeID *uint `query:"employee_id" validate:"omitempty"`
|
||||||
|
PhaseID *uint `query:"phase_id" validate:"omitempty"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,144 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/modules/master/config-checklists/dto"
|
||||||
|
service "gitlab.com/mbugroup/lti-api.git/internal/modules/master/config-checklists/services"
|
||||||
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/config-checklists/validations"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/response"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ConfigChecklistController struct {
|
||||||
|
ConfigChecklistService service.ConfigChecklistService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewConfigChecklistController(configChecklistService service.ConfigChecklistService) *ConfigChecklistController {
|
||||||
|
return &ConfigChecklistController{
|
||||||
|
ConfigChecklistService: configChecklistService,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ConfigChecklistController) GetAll(c *fiber.Ctx) error {
|
||||||
|
query := &validation.Query{
|
||||||
|
Page: c.QueryInt("page", 1),
|
||||||
|
Limit: c.QueryInt("limit", 10),
|
||||||
|
Search: c.Query("search", ""),
|
||||||
|
}
|
||||||
|
|
||||||
|
if query.Page < 1 || query.Limit < 1 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
|
||||||
|
}
|
||||||
|
|
||||||
|
result, totalResults, err := u.ConfigChecklistService.GetAll(c, query)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.SuccessWithPaginate[dto.ConfigChecklistListDTO]{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Get all configChecklists successfully",
|
||||||
|
Meta: response.Meta{
|
||||||
|
Page: query.Page,
|
||||||
|
Limit: query.Limit,
|
||||||
|
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
|
||||||
|
TotalResults: totalResults,
|
||||||
|
},
|
||||||
|
Data: dto.ToConfigChecklistListDTOs(result),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ConfigChecklistController) GetOne(c *fiber.Ctx) error {
|
||||||
|
param := c.Params("id")
|
||||||
|
|
||||||
|
id, err := strconv.Atoi(param)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := u.ConfigChecklistService.GetOne(c, uint(id))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.Success{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Get configChecklist successfully",
|
||||||
|
Data: dto.ToConfigChecklistListDTO(*result),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ConfigChecklistController) CreateOne(c *fiber.Ctx) error {
|
||||||
|
req := new(validation.Create)
|
||||||
|
|
||||||
|
if err := c.BodyParser(req); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := u.ConfigChecklistService.CreateOne(c, req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusCreated).
|
||||||
|
JSON(response.Success{
|
||||||
|
Code: fiber.StatusCreated,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Create configChecklist successfully",
|
||||||
|
Data: dto.ToConfigChecklistListDTO(*result),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ConfigChecklistController) UpdateOne(c *fiber.Ctx) error {
|
||||||
|
req := new(validation.Update)
|
||||||
|
param := c.Params("id")
|
||||||
|
|
||||||
|
id, err := strconv.Atoi(param)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.BodyParser(req); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := u.ConfigChecklistService.UpdateOne(c, req, uint(id))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.Success{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Update configChecklist successfully",
|
||||||
|
Data: dto.ToConfigChecklistListDTO(*result),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ConfigChecklistController) DeleteOne(c *fiber.Ctx) error {
|
||||||
|
param := c.Params("id")
|
||||||
|
|
||||||
|
id, err := strconv.Atoi(param)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := u.ConfigChecklistService.DeleteOne(c, uint(id)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.Common{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Delete configChecklist successfully",
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
)
|
||||||
|
|
||||||
|
// === DTO Structs ===
|
||||||
|
|
||||||
|
type ConfigChecklistRelationDTO struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
Date time.Time `json:"date"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ConfigChecklistListDTO struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
Date time.Time `json:"date"`
|
||||||
|
PercentageThresholdBad int `json:"percentage_threshold_bad"`
|
||||||
|
PercentageThresholdEnough int `json:"percentage_threshold_enough"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ConfigChecklistDetailDTO struct {
|
||||||
|
ConfigChecklistListDTO
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Mapper Functions ===
|
||||||
|
|
||||||
|
func ToConfigChecklistRelationDTO(e entity.ConfigChecklist) ConfigChecklistRelationDTO {
|
||||||
|
return ConfigChecklistRelationDTO{
|
||||||
|
Id: e.Id,
|
||||||
|
Date: e.Date,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToConfigChecklistListDTO(e entity.ConfigChecklist) ConfigChecklistListDTO {
|
||||||
|
return ConfigChecklistListDTO{
|
||||||
|
Id: e.Id,
|
||||||
|
Date: e.Date,
|
||||||
|
PercentageThresholdBad: e.PercentageThresholdBad,
|
||||||
|
PercentageThresholdEnough: e.PercentageThresholdEnough,
|
||||||
|
CreatedAt: e.CreatedAt,
|
||||||
|
UpdatedAt: e.UpdatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToConfigChecklistListDTOs(e []entity.ConfigChecklist) []ConfigChecklistListDTO {
|
||||||
|
result := make([]ConfigChecklistListDTO, len(e))
|
||||||
|
for i, r := range e {
|
||||||
|
result[i] = ToConfigChecklistListDTO(r)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToConfigChecklistDetailDTO(e entity.ConfigChecklist) ConfigChecklistDetailDTO {
|
||||||
|
return ConfigChecklistDetailDTO{
|
||||||
|
ConfigChecklistListDTO: ToConfigChecklistListDTO(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package configChecklists
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
rConfigChecklist "gitlab.com/mbugroup/lti-api.git/internal/modules/master/config-checklists/repositories"
|
||||||
|
sConfigChecklist "gitlab.com/mbugroup/lti-api.git/internal/modules/master/config-checklists/services"
|
||||||
|
|
||||||
|
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
||||||
|
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ConfigChecklistModule struct{}
|
||||||
|
|
||||||
|
func (ConfigChecklistModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
||||||
|
configChecklistRepo := rConfigChecklist.NewConfigChecklistRepository(db)
|
||||||
|
userRepo := rUser.NewUserRepository(db)
|
||||||
|
|
||||||
|
configChecklistService := sConfigChecklist.NewConfigChecklistService(configChecklistRepo, validate)
|
||||||
|
userService := sUser.NewUserService(userRepo, validate)
|
||||||
|
|
||||||
|
ConfigChecklistRoutes(router, userService, configChecklistService)
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ConfigChecklistRepository interface {
|
||||||
|
repository.BaseRepository[entity.ConfigChecklist]
|
||||||
|
}
|
||||||
|
|
||||||
|
type ConfigChecklistRepositoryImpl struct {
|
||||||
|
*repository.BaseRepositoryImpl[entity.ConfigChecklist]
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewConfigChecklistRepository(db *gorm.DB) ConfigChecklistRepository {
|
||||||
|
return &ConfigChecklistRepositoryImpl{
|
||||||
|
BaseRepositoryImpl: repository.NewBaseRepository[entity.ConfigChecklist](db),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package configChecklists
|
||||||
|
|
||||||
|
import (
|
||||||
|
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||||
|
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/config-checklists/controllers"
|
||||||
|
configChecklist "gitlab.com/mbugroup/lti-api.git/internal/modules/master/config-checklists/services"
|
||||||
|
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ConfigChecklistRoutes(v1 fiber.Router, u user.UserService, s configChecklist.ConfigChecklistService) {
|
||||||
|
ctrl := controller.NewConfigChecklistController(s)
|
||||||
|
|
||||||
|
route := v1.Group("/config-checklists")
|
||||||
|
route.Use(m.Auth(u))
|
||||||
|
|
||||||
|
route.Get("/", ctrl.GetAll)
|
||||||
|
route.Post("/", ctrl.CreateOne)
|
||||||
|
route.Get("/:id", ctrl.GetOne)
|
||||||
|
route.Patch("/:id", ctrl.UpdateOne)
|
||||||
|
route.Delete("/:id", ctrl.DeleteOne)
|
||||||
|
}
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/config-checklists/repositories"
|
||||||
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/config-checklists/validations"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
|
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ConfigChecklistService interface {
|
||||||
|
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ConfigChecklist, int64, error)
|
||||||
|
GetOne(ctx *fiber.Ctx, id uint) (*entity.ConfigChecklist, error)
|
||||||
|
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.ConfigChecklist, error)
|
||||||
|
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.ConfigChecklist, error)
|
||||||
|
DeleteOne(ctx *fiber.Ctx, id uint) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type configChecklistService struct {
|
||||||
|
Log *logrus.Logger
|
||||||
|
Validate *validator.Validate
|
||||||
|
Repository repository.ConfigChecklistRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewConfigChecklistService(repo repository.ConfigChecklistRepository, validate *validator.Validate) ConfigChecklistService {
|
||||||
|
return &configChecklistService{
|
||||||
|
Log: utils.Log,
|
||||||
|
Validate: validate,
|
||||||
|
Repository: repo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s configChecklistService) withRelations(db *gorm.DB) *gorm.DB {
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s configChecklistService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ConfigChecklist, int64, error) {
|
||||||
|
if err := s.Validate.Struct(params); err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
offset := (params.Page - 1) * params.Limit
|
||||||
|
|
||||||
|
configChecklists, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
||||||
|
db = s.withRelations(db)
|
||||||
|
return db.Order("date DESC").Order("created_at DESC")
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed to get configChecklists: %+v", err)
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
return configChecklists, total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s configChecklistService) GetOne(c *fiber.Ctx, id uint) (*entity.ConfigChecklist, error) {
|
||||||
|
configChecklist, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, fiber.NewError(fiber.StatusNotFound, "ConfigChecklist not found")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed get configChecklist by id: %+v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return configChecklist, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *configChecklistService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.ConfigChecklist, error) {
|
||||||
|
if err := s.Validate.Struct(req); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
date, err := time.Parse("2006-01-02", req.Date)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "invalid date format, use YYYY-MM-DD")
|
||||||
|
}
|
||||||
|
|
||||||
|
createBody := &entity.ConfigChecklist{
|
||||||
|
Date: date,
|
||||||
|
PercentageThresholdBad: req.PercentageThresholdBad,
|
||||||
|
PercentageThresholdEnough: req.PercentageThresholdEnough,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.Repository.CreateOne(c.Context(), createBody, nil); err != nil {
|
||||||
|
s.Log.Errorf("Failed to create configChecklist: %+v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.GetOne(c, createBody.Id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s configChecklistService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.ConfigChecklist, error) {
|
||||||
|
if err := s.Validate.Struct(req); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
updateBody := make(map[string]any)
|
||||||
|
|
||||||
|
if req.Date != nil {
|
||||||
|
date, err := time.Parse("2006-01-02", *req.Date)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "invalid date format, use YYYY-MM-DD")
|
||||||
|
}
|
||||||
|
updateBody["date"] = date
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.PercentageThresholdBad != nil {
|
||||||
|
updateBody["percentage_threshold_bad"] = *req.PercentageThresholdBad
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.PercentageThresholdEnough != nil {
|
||||||
|
updateBody["percentage_threshold_enough"] = *req.PercentageThresholdEnough
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(updateBody) == 0 {
|
||||||
|
return s.GetOne(c, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, fiber.NewError(fiber.StatusNotFound, "ConfigChecklist not found")
|
||||||
|
}
|
||||||
|
s.Log.Errorf("Failed to update configChecklist: %+v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.GetOne(c, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s configChecklistService) DeleteOne(c *fiber.Ctx, id uint) error {
|
||||||
|
if err := s.Repository.DeleteOne(c.Context(), id); err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return fiber.NewError(fiber.StatusNotFound, "ConfigChecklist not found")
|
||||||
|
}
|
||||||
|
s.Log.Errorf("Failed to delete configChecklist: %+v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package validation
|
||||||
|
|
||||||
|
type Create struct {
|
||||||
|
Date string `json:"date" validate:"required"`
|
||||||
|
PercentageThresholdBad int `json:"percentage_threshold_bad" validate:"required"`
|
||||||
|
PercentageThresholdEnough int `json:"percentage_threshold_enough" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Update struct {
|
||||||
|
Date *string `json:"date,omitempty" validate:"omitempty"`
|
||||||
|
PercentageThresholdBad *int `json:"percentage_threshold_bad,omitempty" validate:"omitempty"`
|
||||||
|
PercentageThresholdEnough *int `json:"percentage_threshold_enough,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"`
|
||||||
|
}
|
||||||
@@ -14,7 +14,7 @@ type Update struct {
|
|||||||
|
|
||||||
type Query struct {
|
type Query struct {
|
||||||
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
|
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
|
||||||
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
|
Limit int `query:"limit" validate:"omitempty,number,min=1,max=500,gt=0"`
|
||||||
Search string `query:"search" validate:"omitempty,max=50"`
|
Search string `query:"search" validate:"omitempty,max=50"`
|
||||||
KandangId *uint `query:"kandang_id" validate:"omitempty"`
|
KandangId *uint `query:"kandang_id" validate:"omitempty"`
|
||||||
IsActive *bool `query:"is_active" validate:"omitempty"`
|
IsActive *bool `query:"is_active" validate:"omitempty"`
|
||||||
|
|||||||
@@ -28,20 +28,12 @@ func (u *PhaseActivityController) GetAll(c *fiber.Ctx) error {
|
|||||||
Limit: c.QueryInt("limit", 10),
|
Limit: c.QueryInt("limit", 10),
|
||||||
Search: c.Query("search", ""),
|
Search: c.Query("search", ""),
|
||||||
}
|
}
|
||||||
|
query.PhaseIDs = c.Query("phase_ids", "")
|
||||||
|
|
||||||
if query.Page < 1 || query.Limit < 1 {
|
if query.Page < 1 || query.Limit < 1 {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
|
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
|
||||||
}
|
}
|
||||||
|
|
||||||
if phaseParam := c.Query("phase_id", ""); phaseParam != "" {
|
|
||||||
id, err := strconv.Atoi(phaseParam)
|
|
||||||
if err != nil || id <= 0 {
|
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "invalid phase_id")
|
|
||||||
}
|
|
||||||
temp := uint(id)
|
|
||||||
query.PhaseId = &temp
|
|
||||||
}
|
|
||||||
|
|
||||||
result, totalResults, err := u.PhaseActivityService.GetAll(c, query)
|
result, totalResults, err := u.PhaseActivityService.GetAll(c, query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
@@ -41,7 +42,8 @@ func NewPhaseActivityService(repo repository.PhaseActivityRepository, phaseRepo
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s phaseActivityService) withRelations(db *gorm.DB) *gorm.DB {
|
func (s phaseActivityService) withRelations(db *gorm.DB) *gorm.DB {
|
||||||
return db
|
return db.Joins("JOIN phases ON phases.id = phase_activities.phase_id").
|
||||||
|
Where("phases.deleted_at IS NULL")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s phaseActivityService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.PhaseActivity, int64, error) {
|
func (s phaseActivityService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.PhaseActivity, int64, error) {
|
||||||
@@ -56,8 +58,11 @@ func (s phaseActivityService) GetAll(c *fiber.Ctx, params *validation.Query) ([]
|
|||||||
if params.Search != "" {
|
if params.Search != "" {
|
||||||
db = db.Where("name LIKE ?", "%"+params.Search+"%")
|
db = db.Where("name LIKE ?", "%"+params.Search+"%")
|
||||||
}
|
}
|
||||||
if params.PhaseId != nil {
|
if params.PhaseIDs != "" {
|
||||||
db = db.Where("phase_id = ?", *params.PhaseId)
|
ids := parseIDs(params.PhaseIDs)
|
||||||
|
if len(ids) > 0 {
|
||||||
|
db = db.Where("phase_id IN ?", ids)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return db.Order("created_at DESC").Order("updated_at DESC")
|
return db.Order("created_at DESC").Order("updated_at DESC")
|
||||||
})
|
})
|
||||||
@@ -165,3 +170,18 @@ func (s phaseActivityService) DeleteOne(c *fiber.Ctx, id uint) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseIDs(raw string) []uint {
|
||||||
|
parts := strings.Split(raw, ",")
|
||||||
|
results := make([]uint, 0, len(parts))
|
||||||
|
for _, part := range parts {
|
||||||
|
value := strings.TrimSpace(part)
|
||||||
|
if value == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if n, err := strconv.ParseUint(value, 10, 64); err == nil {
|
||||||
|
results = append(results, uint(n))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ type Update struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Query struct {
|
type Query struct {
|
||||||
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
|
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
|
||||||
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
|
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
|
||||||
Search string `query:"search" validate:"omitempty,max=50"`
|
Search string `query:"search" validate:"omitempty,max=50"`
|
||||||
PhaseId *uint `query:"phase_id" validate:"omitempty"`
|
PhaseIDs string `query:"phase_ids" validate:"omitempty"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import (
|
|||||||
suppliers "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers"
|
suppliers "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers"
|
||||||
uoms "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms"
|
uoms "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms"
|
||||||
warehouses "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses"
|
warehouses "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses"
|
||||||
|
configChecklists "gitlab.com/mbugroup/lti-api.git/internal/modules/master/config-checklists"
|
||||||
// MODULE IMPORTS
|
// MODULE IMPORTS
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -48,6 +49,7 @@ func RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Valida
|
|||||||
employeess.EmployeesModule{},
|
employeess.EmployeesModule{},
|
||||||
phasess.PhasesModule{},
|
phasess.PhasesModule{},
|
||||||
phaseActivitys.PhaseActivityModule{},
|
phaseActivitys.PhaseActivityModule{},
|
||||||
|
configChecklists.ConfigChecklistModule{},
|
||||||
// MODULE REGISTRY
|
// MODULE REGISTRY
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
|
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/modules/production/uniformities/dto"
|
"gitlab.com/mbugroup/lti-api.git/internal/modules/production/uniformities/dto"
|
||||||
service "gitlab.com/mbugroup/lti-api.git/internal/modules/production/uniformities/services"
|
service "gitlab.com/mbugroup/lti-api.git/internal/modules/production/uniformities/services"
|
||||||
|
utypes "gitlab.com/mbugroup/lti-api.git/internal/modules/production/uniformities/types"
|
||||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/uniformities/validations"
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/uniformities/validations"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/response"
|
"gitlab.com/mbugroup/lti-api.git/internal/response"
|
||||||
|
|
||||||
@@ -40,6 +41,13 @@ func (u *UniformityController) GetAll(c *fiber.Ctx) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
var charts map[uint]utypes.UniformityChartData
|
||||||
|
if query.WithChart {
|
||||||
|
charts, err = u.UniformityService.MapCharts(c, result)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return c.Status(fiber.StatusOK).
|
return c.Status(fiber.StatusOK).
|
||||||
JSON(response.SuccessWithPaginate[dto.UniformityListDTO]{
|
JSON(response.SuccessWithPaginate[dto.UniformityListDTO]{
|
||||||
@@ -51,13 +59,9 @@ func (u *UniformityController) GetAll(c *fiber.Ctx) error {
|
|||||||
Limit: query.Limit,
|
Limit: query.Limit,
|
||||||
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
|
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
|
||||||
TotalResults: totalResults,
|
TotalResults: totalResults,
|
||||||
Filters: fiber.Map{
|
Filters: dto.BuildUniformityFilters(query),
|
||||||
"location_id": "",
|
|
||||||
"project_flock_id": "",
|
|
||||||
"status": "Pengajuan",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
Data: dto.ToUniformityListDTOsWithStandard(result, standards, documents),
|
Data: dto.ToUniformityListDTOsWithStandard(result, standards, documents, charts),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,7 +77,7 @@ func (u *UniformityController) GetOne(c *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
withDetails := c.QueryBool("with_details", false)
|
withDetails := c.QueryBool("with_details", false)
|
||||||
calculation := service.UniformityCalculation{}
|
calculation := utypes.UniformityCalculation{}
|
||||||
var document *entity.Document
|
var document *entity.Document
|
||||||
var documentURL string
|
var documentURL string
|
||||||
var meanWeight float64
|
var meanWeight float64
|
||||||
@@ -87,7 +91,7 @@ func (u *UniformityController) GetOne(c *fiber.Ctx) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
calculation = service.UniformityCalculation{
|
calculation = utypes.UniformityCalculation{
|
||||||
ChickQtyOfWeight: result.ChickQtyOfWeight,
|
ChickQtyOfWeight: result.ChickQtyOfWeight,
|
||||||
MeanWeight: meanWeight,
|
MeanWeight: meanWeight,
|
||||||
MeanDown: result.MeanDown,
|
MeanDown: result.MeanDown,
|
||||||
@@ -229,7 +233,7 @@ func (u *UniformityController) UpdateOne(c *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
calculation := service.UniformityCalculation{
|
calculation := utypes.UniformityCalculation{
|
||||||
ChickQtyOfWeight: result.ChickQtyOfWeight,
|
ChickQtyOfWeight: result.ChickQtyOfWeight,
|
||||||
MeanWeight: math.Round(result.MeanUp / 1.10),
|
MeanWeight: math.Round(result.MeanUp / 1.10),
|
||||||
MeanDown: result.MeanDown,
|
MeanDown: result.MeanDown,
|
||||||
|
|||||||
@@ -5,7 +5,11 @@ import (
|
|||||||
|
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto"
|
approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto"
|
||||||
service "gitlab.com/mbugroup/lti-api.git/internal/modules/production/uniformities/services"
|
utypes "gitlab.com/mbugroup/lti-api.git/internal/modules/production/uniformities/types"
|
||||||
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/uniformities/validations"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
type UniformitySamplingDTO struct {
|
type UniformitySamplingDTO struct {
|
||||||
@@ -49,13 +53,13 @@ type UniformityInfoDTO struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type UniformityDetailDTO struct {
|
type UniformityDetailDTO struct {
|
||||||
Id uint `json:"id"`
|
Id uint `json:"id"`
|
||||||
InfoUmum UniformityInfoDTO `json:"info_umum"`
|
InfoUmum UniformityInfoDTO `json:"info_umum"`
|
||||||
Sampling UniformitySamplingDTO `json:"sampling"`
|
Sampling UniformitySamplingDTO `json:"sampling"`
|
||||||
Result UniformityResultDTO `json:"result"`
|
Result UniformityResultDTO `json:"result"`
|
||||||
Standard *UniformityStandardDTO `json:"standard"`
|
Standard *UniformityStandardDTO `json:"standard"`
|
||||||
LatestApproval *approvalDTO.ApprovalRelationDTO `json:"latest_approval"`
|
LatestApproval *approvalDTO.ApprovalRelationDTO `json:"latest_approval"`
|
||||||
UniformityDetails []UniformityDetailItemDTO `json:"uniformity_details"`
|
UniformityDetails []UniformityDetailItemDTO `json:"uniformity_details"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UniformityListDTO struct {
|
type UniformityListDTO struct {
|
||||||
@@ -76,6 +80,7 @@ type UniformityListDTO struct {
|
|||||||
MeanDown float64 `json:"mean_down"`
|
MeanDown float64 `json:"mean_down"`
|
||||||
StandardMeanWeight *float64 `json:"standard_mean_weight"`
|
StandardMeanWeight *float64 `json:"standard_mean_weight"`
|
||||||
StandardUniformity *float64 `json:"standard_uniformity"`
|
StandardUniformity *float64 `json:"standard_uniformity"`
|
||||||
|
ChartData *utypes.UniformityChartData `json:"chart_data,omitempty"`
|
||||||
CreatedBy uint `json:"created_by"`
|
CreatedBy uint `json:"created_by"`
|
||||||
LatestApproval *approvalDTO.ApprovalRelationDTO `json:"latest_approval"`
|
LatestApproval *approvalDTO.ApprovalRelationDTO `json:"latest_approval"`
|
||||||
}
|
}
|
||||||
@@ -87,7 +92,7 @@ func NewDocumentForResponse(name string) *entity.Document {
|
|||||||
return &entity.Document{Name: name}
|
return &entity.Document{Name: name}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ToUniformityVerificationDTO(calc service.UniformityCalculation) UniformityVerificationDTO {
|
func ToUniformityVerificationDTO(calc utypes.UniformityCalculation) UniformityVerificationDTO {
|
||||||
return UniformityVerificationDTO{
|
return UniformityVerificationDTO{
|
||||||
Sampling: toUniformitySamplingDTO(calc),
|
Sampling: toUniformitySamplingDTO(calc),
|
||||||
Result: toUniformityResultDTO(calc),
|
Result: toUniformityResultDTO(calc),
|
||||||
@@ -97,7 +102,7 @@ func ToUniformityVerificationDTO(calc service.UniformityCalculation) UniformityV
|
|||||||
|
|
||||||
func ToUniformityDetailDTO(
|
func ToUniformityDetailDTO(
|
||||||
entityData entity.ProjectFlockKandangUniformity,
|
entityData entity.ProjectFlockKandangUniformity,
|
||||||
calc service.UniformityCalculation,
|
calc utypes.UniformityCalculation,
|
||||||
document *entity.Document,
|
document *entity.Document,
|
||||||
documentURL string,
|
documentURL string,
|
||||||
standard *UniformityStandardDTO,
|
standard *UniformityStandardDTO,
|
||||||
@@ -171,8 +176,9 @@ func ToUniformityListDTOs(items []entity.ProjectFlockKandangUniformity) []Unifor
|
|||||||
|
|
||||||
func ToUniformityListDTOsWithStandard(
|
func ToUniformityListDTOsWithStandard(
|
||||||
items []entity.ProjectFlockKandangUniformity,
|
items []entity.ProjectFlockKandangUniformity,
|
||||||
standards map[uint]service.UniformityStandard,
|
standards map[uint]utypes.UniformityStandard,
|
||||||
documentNames map[uint]string,
|
documentNames map[uint]string,
|
||||||
|
charts map[uint]utypes.UniformityChartData,
|
||||||
) []UniformityListDTO {
|
) []UniformityListDTO {
|
||||||
result := ToUniformityListDTOs(items)
|
result := ToUniformityListDTOs(items)
|
||||||
if len(result) == 0 || len(standards) == 0 {
|
if len(result) == 0 || len(standards) == 0 {
|
||||||
@@ -180,6 +186,10 @@ func ToUniformityListDTOsWithStandard(
|
|||||||
if name, ok := documentNames[result[i].Id]; ok {
|
if name, ok := documentNames[result[i].Id]; ok {
|
||||||
result[i].FileName = name
|
result[i].FileName = name
|
||||||
}
|
}
|
||||||
|
if chart, ok := charts[result[i].Id]; ok {
|
||||||
|
chartCopy := chart
|
||||||
|
result[i].ChartData = &chartCopy
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
@@ -192,11 +202,15 @@ func ToUniformityListDTOsWithStandard(
|
|||||||
if name, ok := documentNames[result[i].Id]; ok {
|
if name, ok := documentNames[result[i].Id]; ok {
|
||||||
result[i].FileName = name
|
result[i].FileName = name
|
||||||
}
|
}
|
||||||
|
if chart, ok := charts[result[i].Id]; ok {
|
||||||
|
chartCopy := chart
|
||||||
|
result[i].ChartData = &chartCopy
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func toUniformitySamplingDTO(calc service.UniformityCalculation) UniformitySamplingDTO {
|
func toUniformitySamplingDTO(calc utypes.UniformityCalculation) UniformitySamplingDTO {
|
||||||
return UniformitySamplingDTO{
|
return UniformitySamplingDTO{
|
||||||
ChickQtyOfWeight: calc.ChickQtyOfWeight,
|
ChickQtyOfWeight: calc.ChickQtyOfWeight,
|
||||||
MeanWeight: calc.MeanWeight,
|
MeanWeight: calc.MeanWeight,
|
||||||
@@ -205,7 +219,7 @@ func toUniformitySamplingDTO(calc service.UniformityCalculation) UniformitySampl
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func toUniformityResultDTO(calc service.UniformityCalculation) UniformityResultDTO {
|
func toUniformityResultDTO(calc utypes.UniformityCalculation) UniformityResultDTO {
|
||||||
return UniformityResultDTO{
|
return UniformityResultDTO{
|
||||||
UniformQty: calc.UniformQty,
|
UniformQty: calc.UniformQty,
|
||||||
OutsideQty: calc.OutsideQty,
|
OutsideQty: calc.OutsideQty,
|
||||||
@@ -214,7 +228,7 @@ func toUniformityResultDTO(calc service.UniformityCalculation) UniformityResultD
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func toUniformityDetailItemsDTO(calc service.UniformityCalculation) []UniformityDetailItemDTO {
|
func toUniformityDetailItemsDTO(calc utypes.UniformityCalculation) []UniformityDetailItemDTO {
|
||||||
result := make([]UniformityDetailItemDTO, len(calc.Details))
|
result := make([]UniformityDetailItemDTO, len(calc.Details))
|
||||||
for i, item := range calc.Details {
|
for i, item := range calc.Details {
|
||||||
result[i] = UniformityDetailItemDTO{
|
result[i] = UniformityDetailItemDTO{
|
||||||
@@ -254,5 +268,18 @@ func formatUniformityDate(date *time.Time) string {
|
|||||||
if date == nil || date.IsZero() {
|
if date == nil || date.IsZero() {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
return date.Format("2006-01-02")
|
return utils.FormatDate(*date)
|
||||||
|
}
|
||||||
|
|
||||||
|
func BuildUniformityFilters(query *validation.Query) fiber.Map {
|
||||||
|
if query == nil {
|
||||||
|
return fiber.Map{}
|
||||||
|
}
|
||||||
|
return fiber.Map{
|
||||||
|
"project_flock_kandang_id": query.ProjectFlockKandangId,
|
||||||
|
"start_date": query.StartDate,
|
||||||
|
"end_date": query.EndDate,
|
||||||
|
"with_chart": query.WithChart,
|
||||||
|
"status": "Pengajuan",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,14 +2,18 @@ package repository
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/uniformities/validations"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type UniformityRepository interface {
|
type UniformityRepository interface {
|
||||||
repository.BaseRepository[entity.ProjectFlockKandangUniformity]
|
repository.BaseRepository[entity.ProjectFlockKandangUniformity]
|
||||||
|
GetAllWithFilters(ctx context.Context, offset, limit int, params *validation.Query) ([]entity.ProjectFlockKandangUniformity, int64, error)
|
||||||
|
WithDefaultRelations() func(*gorm.DB) *gorm.DB
|
||||||
DeleteByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) error
|
DeleteByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) error
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,6 +27,46 @@ func NewUniformityRepository(db *gorm.DB) UniformityRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *UniformityRepositoryImpl) GetAllWithFilters(ctx context.Context, offset, limit int, params *validation.Query) ([]entity.ProjectFlockKandangUniformity, int64, error) {
|
||||||
|
return r.GetAll(ctx, offset, limit, func(db *gorm.DB) *gorm.DB {
|
||||||
|
return r.applyQueryFilters(r.WithDefaultRelations()(db), params)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *UniformityRepositoryImpl) WithDefaultRelations() func(*gorm.DB) *gorm.DB {
|
||||||
|
return func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.
|
||||||
|
Preload("ProjectFlockKandang.ProjectFlock.Location").
|
||||||
|
Preload("ProjectFlockKandang.Kandang.Location")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *UniformityRepositoryImpl) applyQueryFilters(db *gorm.DB, params *validation.Query) *gorm.DB {
|
||||||
|
if params == nil {
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
|
||||||
|
if params.ProjectFlockKandangId != 0 {
|
||||||
|
db = db.Where("project_flock_kandang_id = ?", params.ProjectFlockKandangId)
|
||||||
|
}
|
||||||
|
if params.Week != 0 {
|
||||||
|
db = db.Where("week = ?", params.Week)
|
||||||
|
}
|
||||||
|
|
||||||
|
startDateValue, endDateValue, err := validation.ParseDateRange(params.StartDate, params.EndDate)
|
||||||
|
if err == nil {
|
||||||
|
if startDateValue != nil && endDateValue != nil {
|
||||||
|
db = db.Where("uniform_date >= ? AND uniform_date < ?", *startDateValue, endDateValue.Add(24*time.Hour))
|
||||||
|
} else if startDateValue != nil {
|
||||||
|
db = db.Where("uniform_date >= ?", *startDateValue)
|
||||||
|
} else if endDateValue != nil {
|
||||||
|
db = db.Where("uniform_date < ?", endDateValue.Add(24*time.Hour))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return db.Order("uniform_date DESC").Order("id DESC")
|
||||||
|
}
|
||||||
|
|
||||||
func (r *UniformityRepositoryImpl) DeleteByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) error {
|
func (r *UniformityRepositoryImpl) DeleteByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) error {
|
||||||
if len(projectFlockKandangIDs) == 0 {
|
if len(projectFlockKandangIDs) == 0 {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -1,200 +0,0 @@
|
|||||||
package service
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
"mime/multipart"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
"github.com/xuri/excelize/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
type BodyWeightExcelRow struct {
|
|
||||||
No int `json:"no"`
|
|
||||||
Weight float64 `json:"weight"`
|
|
||||||
Range string `json:"range,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s uniformityService) ParseBodyWeightExcel(_ *fiber.Ctx, file *multipart.FileHeader) ([]BodyWeightExcelRow, error) {
|
|
||||||
if file == nil {
|
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "file is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
reader, err := file.Open()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "failed to open file")
|
|
||||||
}
|
|
||||||
defer reader.Close()
|
|
||||||
|
|
||||||
rows, err := parseBodyWeightExcelReader(reader)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return rows, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseBodyWeightExcelReader(reader io.Reader) ([]BodyWeightExcelRow, error) {
|
|
||||||
xlsx, err := excelize.OpenReader(reader)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "failed to read excel file")
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
_ = xlsx.Close()
|
|
||||||
}()
|
|
||||||
|
|
||||||
sheets := xlsx.GetSheetList()
|
|
||||||
if len(sheets) == 0 {
|
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "no sheets found in file")
|
|
||||||
}
|
|
||||||
|
|
||||||
sheetName := sheets[0]
|
|
||||||
if len(sheets) > 1 {
|
|
||||||
sheetName = sheets[1]
|
|
||||||
}
|
|
||||||
|
|
||||||
rows, err := xlsx.GetRows(sheetName, excelize.Options{RawCellValue: true})
|
|
||||||
if err != nil {
|
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "failed to read sheet rows")
|
|
||||||
}
|
|
||||||
|
|
||||||
return parseBodyWeightRows(rows)
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseBodyWeightRows(rows [][]string) ([]BodyWeightExcelRow, error) {
|
|
||||||
headerRowIdx, noCol, bwCol, rangeCol := findBodyWeightHeader(rows)
|
|
||||||
if headerRowIdx < 0 || bwCol < 0 {
|
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "header BW not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
result := make([]BodyWeightExcelRow, 0)
|
|
||||||
lastNo := 0
|
|
||||||
|
|
||||||
for i := headerRowIdx + 1; i < len(rows); i++ {
|
|
||||||
row := rows[i]
|
|
||||||
weightStr := cellAt(row, bwCol)
|
|
||||||
weightVal, ok := parseNumber(weightStr)
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
noVal := 0
|
|
||||||
if noCol >= 0 {
|
|
||||||
if parsed, ok := parseNumber(cellAt(row, noCol)); ok {
|
|
||||||
noVal = int(parsed)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if noVal <= 0 {
|
|
||||||
noVal = lastNo + 1
|
|
||||||
}
|
|
||||||
if noVal > lastNo {
|
|
||||||
lastNo = noVal
|
|
||||||
}
|
|
||||||
|
|
||||||
rangeVal := ""
|
|
||||||
if rangeCol >= 0 {
|
|
||||||
rangeVal = strings.TrimSpace(cellAt(row, rangeCol))
|
|
||||||
}
|
|
||||||
|
|
||||||
rowPayload := BodyWeightExcelRow{
|
|
||||||
No: noVal,
|
|
||||||
Weight: weightVal,
|
|
||||||
Range: rangeVal,
|
|
||||||
}
|
|
||||||
if rowPayload.No <= 0 || rowPayload.Weight <= 0 {
|
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "invalid body weight row data")
|
|
||||||
}
|
|
||||||
|
|
||||||
result = append(result, rowPayload)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(result) == 0 {
|
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "no body weight data found")
|
|
||||||
}
|
|
||||||
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func findBodyWeightHeader(rows [][]string) (rowIdx int, noCol int, bwCol int, rangeCol int) {
|
|
||||||
rowIdx = -1
|
|
||||||
noCol = -1
|
|
||||||
bwCol = -1
|
|
||||||
rangeCol = -1
|
|
||||||
|
|
||||||
for i, row := range rows {
|
|
||||||
tempNo := -1
|
|
||||||
tempBW := -1
|
|
||||||
tempRange := -1
|
|
||||||
for j, cell := range row {
|
|
||||||
label := normalizeHeader(cell)
|
|
||||||
switch label {
|
|
||||||
case "no":
|
|
||||||
tempNo = j
|
|
||||||
case "bw":
|
|
||||||
tempBW = j
|
|
||||||
case "outsiderange":
|
|
||||||
tempRange = j
|
|
||||||
default:
|
|
||||||
if strings.HasPrefix(label, "bw") {
|
|
||||||
tempBW = j
|
|
||||||
} else if strings.HasPrefix(label, "no") {
|
|
||||||
tempNo = j
|
|
||||||
} else if strings.Contains(label, "range") {
|
|
||||||
tempRange = j
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if tempBW >= 0 {
|
|
||||||
rowIdx = i
|
|
||||||
bwCol = tempBW
|
|
||||||
noCol = tempNo
|
|
||||||
rangeCol = tempRange
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return rowIdx, noCol, bwCol, rangeCol
|
|
||||||
}
|
|
||||||
|
|
||||||
func cellAt(row []string, idx int) string {
|
|
||||||
if idx < 0 || idx >= len(row) {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return strings.TrimSpace(row[idx])
|
|
||||||
}
|
|
||||||
|
|
||||||
func normalizeHeader(value string) string {
|
|
||||||
trimmed := strings.ToLower(strings.TrimSpace(value))
|
|
||||||
if trimmed == "" {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
var b strings.Builder
|
|
||||||
for _, r := range trimmed {
|
|
||||||
if r >= 'a' && r <= 'z' {
|
|
||||||
b.WriteRune(r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return b.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseNumber(value string) (float64, bool) {
|
|
||||||
trimmed := strings.TrimSpace(value)
|
|
||||||
if trimmed == "" {
|
|
||||||
return 0, false
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.Contains(trimmed, ",") {
|
|
||||||
if strings.Contains(trimmed, ".") {
|
|
||||||
trimmed = strings.ReplaceAll(trimmed, ",", "")
|
|
||||||
} else {
|
|
||||||
trimmed = strings.ReplaceAll(trimmed, ",", ".")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
parsed, err := strconv.ParseFloat(trimmed, 64)
|
|
||||||
if err != nil {
|
|
||||||
return 0, false
|
|
||||||
}
|
|
||||||
return parsed, true
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,433 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"math"
|
||||||
|
"mime/multipart"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
utypes "gitlab.com/mbugroup/lti-api.git/internal/modules/production/uniformities/types"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/xuri/excelize/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type BodyWeightExcelRow struct {
|
||||||
|
No int `json:"no"`
|
||||||
|
Weight float64 `json:"weight"`
|
||||||
|
Range string `json:"range,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s uniformityService) ParseBodyWeightExcel(_ *fiber.Ctx, file *multipart.FileHeader) ([]BodyWeightExcelRow, error) {
|
||||||
|
if file == nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "file is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
reader, err := file.Open()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "failed to open file")
|
||||||
|
}
|
||||||
|
defer reader.Close()
|
||||||
|
|
||||||
|
rows, err := parseBodyWeightExcelReader(reader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseBodyWeightExcelReader(reader io.Reader) ([]BodyWeightExcelRow, error) {
|
||||||
|
xlsx, err := excelize.OpenReader(reader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "failed to read excel file")
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
_ = xlsx.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
sheets := xlsx.GetSheetList()
|
||||||
|
if len(sheets) == 0 {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "no sheets found in file")
|
||||||
|
}
|
||||||
|
|
||||||
|
sheetName := sheets[0]
|
||||||
|
if len(sheets) > 1 {
|
||||||
|
sheetName = sheets[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := xlsx.GetRows(sheetName, excelize.Options{RawCellValue: true})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "failed to read sheet rows")
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseBodyWeightRows(rows)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseBodyWeightRows(rows [][]string) ([]BodyWeightExcelRow, error) {
|
||||||
|
headerRowIdx, noCol, bwCol, rangeCol := findBodyWeightHeader(rows)
|
||||||
|
if headerRowIdx < 0 || bwCol < 0 {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "header BW not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make([]BodyWeightExcelRow, 0)
|
||||||
|
lastNo := 0
|
||||||
|
|
||||||
|
for i := headerRowIdx + 1; i < len(rows); i++ {
|
||||||
|
row := rows[i]
|
||||||
|
weightStr := cellAt(row, bwCol)
|
||||||
|
weightVal, ok := parseNumber(weightStr)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
noVal := 0
|
||||||
|
if noCol >= 0 {
|
||||||
|
if parsed, ok := parseNumber(cellAt(row, noCol)); ok {
|
||||||
|
noVal = int(parsed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if noVal <= 0 {
|
||||||
|
noVal = lastNo + 1
|
||||||
|
}
|
||||||
|
if noVal > lastNo {
|
||||||
|
lastNo = noVal
|
||||||
|
}
|
||||||
|
|
||||||
|
rangeVal := ""
|
||||||
|
if rangeCol >= 0 {
|
||||||
|
rangeVal = strings.TrimSpace(cellAt(row, rangeCol))
|
||||||
|
}
|
||||||
|
|
||||||
|
rowPayload := BodyWeightExcelRow{
|
||||||
|
No: noVal,
|
||||||
|
Weight: weightVal,
|
||||||
|
Range: rangeVal,
|
||||||
|
}
|
||||||
|
if rowPayload.No <= 0 || rowPayload.Weight <= 0 {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "invalid body weight row data")
|
||||||
|
}
|
||||||
|
|
||||||
|
result = append(result, rowPayload)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(result) == 0 {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "no body weight data found")
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func findBodyWeightHeader(rows [][]string) (rowIdx int, noCol int, bwCol int, rangeCol int) {
|
||||||
|
rowIdx = -1
|
||||||
|
noCol = -1
|
||||||
|
bwCol = -1
|
||||||
|
rangeCol = -1
|
||||||
|
|
||||||
|
for i, row := range rows {
|
||||||
|
tempNo := -1
|
||||||
|
tempBW := -1
|
||||||
|
tempRange := -1
|
||||||
|
for j, cell := range row {
|
||||||
|
label := normalizeHeader(cell)
|
||||||
|
switch label {
|
||||||
|
case "no":
|
||||||
|
tempNo = j
|
||||||
|
case "bw":
|
||||||
|
tempBW = j
|
||||||
|
case "outsiderange":
|
||||||
|
tempRange = j
|
||||||
|
default:
|
||||||
|
if strings.HasPrefix(label, "bw") {
|
||||||
|
tempBW = j
|
||||||
|
} else if strings.HasPrefix(label, "no") {
|
||||||
|
tempNo = j
|
||||||
|
} else if strings.Contains(label, "range") {
|
||||||
|
tempRange = j
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if tempBW >= 0 {
|
||||||
|
rowIdx = i
|
||||||
|
bwCol = tempBW
|
||||||
|
noCol = tempNo
|
||||||
|
rangeCol = tempRange
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return rowIdx, noCol, bwCol, rangeCol
|
||||||
|
}
|
||||||
|
|
||||||
|
func cellAt(row []string, idx int) string {
|
||||||
|
if idx < 0 || idx >= len(row) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(row[idx])
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeHeader(value string) string {
|
||||||
|
trimmed := strings.ToLower(strings.TrimSpace(value))
|
||||||
|
if trimmed == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
var b strings.Builder
|
||||||
|
for _, r := range trimmed {
|
||||||
|
if r >= 'a' && r <= 'z' {
|
||||||
|
b.WriteRune(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseNumber(value string) (float64, bool) {
|
||||||
|
trimmed := strings.TrimSpace(value)
|
||||||
|
if trimmed == "" {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(trimmed, ",") {
|
||||||
|
if strings.Contains(trimmed, ".") {
|
||||||
|
trimmed = strings.ReplaceAll(trimmed, ",", "")
|
||||||
|
} else {
|
||||||
|
trimmed = strings.ReplaceAll(trimmed, ",", ".")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parsed, err := strconv.ParseFloat(trimmed, 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
return parsed, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func computeUniformity(rows []BodyWeightExcelRow) (utypes.UniformityCalculation, error) {
|
||||||
|
weights := make([]float64, 0, len(rows))
|
||||||
|
details := make([]utypes.UniformityDetailItem, 0, len(rows))
|
||||||
|
hasRangeLabels := false
|
||||||
|
for idx, row := range rows {
|
||||||
|
if row.Weight <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
id := row.No
|
||||||
|
if id <= 0 {
|
||||||
|
id = idx + 1
|
||||||
|
}
|
||||||
|
weights = append(weights, row.Weight)
|
||||||
|
rangeLabel := strings.TrimSpace(row.Range)
|
||||||
|
if rangeLabel != "" {
|
||||||
|
upper := strings.ToUpper(rangeLabel)
|
||||||
|
if upper == "HIGH" || upper == "LOW" {
|
||||||
|
hasRangeLabels = true
|
||||||
|
}
|
||||||
|
rangeLabel = upper
|
||||||
|
}
|
||||||
|
details = append(details, utypes.UniformityDetailItem{
|
||||||
|
Id: id,
|
||||||
|
Weight: row.Weight,
|
||||||
|
Range: rangeLabel,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
total := float64(len(weights))
|
||||||
|
if total == 0 {
|
||||||
|
return utypes.UniformityCalculation{}, fiber.NewError(fiber.StatusBadRequest, "no body weight data found")
|
||||||
|
}
|
||||||
|
|
||||||
|
var sum float64
|
||||||
|
for _, w := range weights {
|
||||||
|
sum += w
|
||||||
|
}
|
||||||
|
mean := sum / total
|
||||||
|
meanUpThreshold := roundToPrecision(mean*1.10, 3)
|
||||||
|
meanDownThreshold := roundToPrecision(mean*0.90, 3)
|
||||||
|
|
||||||
|
var uniformCount float64
|
||||||
|
for i := range details {
|
||||||
|
if hasRangeLabels {
|
||||||
|
if details[i].Range == "HIGH" || details[i].Range == "LOW" {
|
||||||
|
details[i].Range = "Outside"
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
details[i].Range = "Ideal"
|
||||||
|
uniformCount++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if details[i].Weight > meanUpThreshold || details[i].Weight < meanDownThreshold {
|
||||||
|
details[i].Range = "Outside"
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
details[i].Range = "Ideal"
|
||||||
|
uniformCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
var deviationSum float64
|
||||||
|
for _, w := range weights {
|
||||||
|
deviation := w - mean
|
||||||
|
deviationSum += deviation * deviation
|
||||||
|
}
|
||||||
|
stdDev := math.Sqrt(deviationSum / total)
|
||||||
|
|
||||||
|
cv := 0.0
|
||||||
|
if mean != 0 {
|
||||||
|
cv = (stdDev / mean) * 100
|
||||||
|
}
|
||||||
|
|
||||||
|
outsideCount := total - uniformCount
|
||||||
|
uniformity := 0.0
|
||||||
|
if total > 0 {
|
||||||
|
uniformity = (uniformCount / total) * 100
|
||||||
|
}
|
||||||
|
|
||||||
|
return utypes.UniformityCalculation{
|
||||||
|
ChickQtyOfWeight: total,
|
||||||
|
MeanWeight: roundToPrecision(mean, 0),
|
||||||
|
MeanDown: roundToPrecision(meanDownThreshold, 0),
|
||||||
|
MeanUp: roundToPrecision(meanUpThreshold, 0),
|
||||||
|
UniformQty: uniformCount,
|
||||||
|
OutsideQty: outsideCount,
|
||||||
|
Uniformity: roundToPrecision(uniformity, 0),
|
||||||
|
Cv: roundToPrecision(cv, 1),
|
||||||
|
Details: details,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractWeights(rows []BodyWeightExcelRow) []float64 {
|
||||||
|
weights := make([]float64, 0, len(rows))
|
||||||
|
for _, row := range rows {
|
||||||
|
if row.Weight <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
weights = append(weights, row.Weight)
|
||||||
|
}
|
||||||
|
return weights
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildChartWeekSummary(weights []float64) utypes.UniformityChartWeek {
|
||||||
|
if len(weights) == 0 {
|
||||||
|
return utypes.UniformityChartWeek{
|
||||||
|
HasData: false,
|
||||||
|
WeightDistribution: []utypes.UniformityChartRange{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
minWeight := weights[0]
|
||||||
|
maxWeight := weights[0]
|
||||||
|
var sum float64
|
||||||
|
for _, w := range weights {
|
||||||
|
sum += w
|
||||||
|
if w < minWeight {
|
||||||
|
minWeight = w
|
||||||
|
}
|
||||||
|
if w > maxWeight {
|
||||||
|
maxWeight = w
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mean := sum / float64(len(weights))
|
||||||
|
|
||||||
|
idealMin := roundToPrecision(mean*0.90, 0)
|
||||||
|
idealMax := roundToPrecision(mean*1.10, 0)
|
||||||
|
idealCount := 0.0
|
||||||
|
for _, w := range weights {
|
||||||
|
if w >= idealMin && w <= idealMax {
|
||||||
|
idealCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const bucketSize = 5.0
|
||||||
|
start := math.Floor(minWeight/bucketSize) * bucketSize
|
||||||
|
end := math.Floor(maxWeight/bucketSize) * bucketSize
|
||||||
|
|
||||||
|
distribution := make([]utypes.UniformityChartRange, 0)
|
||||||
|
for bucket := start; bucket <= end; bucket += bucketSize {
|
||||||
|
minBucket := bucket
|
||||||
|
maxBucket := bucket + bucketSize - 1
|
||||||
|
count := 0.0
|
||||||
|
bucketWeights := make([]float64, 0)
|
||||||
|
idealWeights := make([]float64, 0)
|
||||||
|
outsideWeights := make([]float64, 0)
|
||||||
|
for _, w := range weights {
|
||||||
|
if w >= minBucket && w < minBucket+bucketSize {
|
||||||
|
count++
|
||||||
|
bucketWeights = append(bucketWeights, w)
|
||||||
|
if w >= idealMin && w <= idealMax {
|
||||||
|
idealWeights = append(idealWeights, w)
|
||||||
|
} else {
|
||||||
|
outsideWeights = append(outsideWeights, w)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
idealRangeLabel := rangeFromValues(idealWeights)
|
||||||
|
outsideRangeLabel := rangeFromValues(outsideWeights)
|
||||||
|
isIdealRange := idealRangeLabel != ""
|
||||||
|
distribution = append(distribution, utypes.UniformityChartRange{
|
||||||
|
Range: fmt.Sprintf("%d-%d", int(minBucket), int(maxBucket)),
|
||||||
|
MinWeight: minBucket,
|
||||||
|
MaxWeight: maxBucket,
|
||||||
|
BirdCount: count,
|
||||||
|
IsIdealRange: isIdealRange,
|
||||||
|
IdealRange: idealRangeLabel,
|
||||||
|
OutsideRange: outsideRangeLabel,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
statistics := &utypes.UniformityChartStatistics{
|
||||||
|
MinWeight: roundToPrecision(minWeight, 0),
|
||||||
|
MaxWeight: roundToPrecision(maxWeight, 0),
|
||||||
|
AverageWeight: roundToPrecision(mean, 1),
|
||||||
|
TotalBirdsMeasured: float64(len(weights)),
|
||||||
|
}
|
||||||
|
|
||||||
|
return utypes.UniformityChartWeek{
|
||||||
|
HasData: true,
|
||||||
|
WeightDistribution: distribution,
|
||||||
|
IdealRange: &utypes.UniformityChartIdealRange{
|
||||||
|
MinWeight: idealMin,
|
||||||
|
MaxWeight: idealMax,
|
||||||
|
TotalIdealBirds: idealCount,
|
||||||
|
},
|
||||||
|
Statistics: statistics,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func roundToPrecision(value float64, precision int) float64 {
|
||||||
|
if precision < 0 {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
scale := math.Pow10(precision)
|
||||||
|
scaled := value * scale
|
||||||
|
fraction := scaled - math.Floor(scaled)
|
||||||
|
if fraction >= 0.5 {
|
||||||
|
return math.Ceil(scaled) / scale
|
||||||
|
}
|
||||||
|
return math.Floor(scaled) / scale
|
||||||
|
}
|
||||||
|
|
||||||
|
func rangeFromValues(values []float64) string {
|
||||||
|
if len(values) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
minValue := values[0]
|
||||||
|
maxValue := values[0]
|
||||||
|
for _, v := range values[1:] {
|
||||||
|
if v < minValue {
|
||||||
|
minValue = v
|
||||||
|
}
|
||||||
|
if v > maxValue {
|
||||||
|
maxValue = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return formatRange(minValue, maxValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatRange(minValue, maxValue float64) string {
|
||||||
|
minInt := int(math.Round(minValue))
|
||||||
|
maxInt := int(math.Round(maxValue))
|
||||||
|
if minInt == maxInt {
|
||||||
|
return fmt.Sprintf("%d", minInt)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%d-%d", minInt, maxInt)
|
||||||
|
}
|
||||||
@@ -2,11 +2,12 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -17,6 +18,7 @@ import (
|
|||||||
rProductionStandard "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/repositories"
|
rProductionStandard "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/repositories"
|
||||||
rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||||
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/uniformities/repositories"
|
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/uniformities/repositories"
|
||||||
|
utypes "gitlab.com/mbugroup/lti-api.git/internal/modules/production/uniformities/types"
|
||||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/uniformities/validations"
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/uniformities/validations"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
|
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
|
||||||
@@ -31,17 +33,18 @@ type UniformityService interface {
|
|||||||
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlockKandangUniformity, int64, error)
|
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlockKandangUniformity, int64, error)
|
||||||
GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectFlockKandangUniformity, error)
|
GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectFlockKandangUniformity, error)
|
||||||
GetSummary(ctx *fiber.Ctx, id uint) (*entity.ProjectFlockKandangUniformity, error)
|
GetSummary(ctx *fiber.Ctx, id uint) (*entity.ProjectFlockKandangUniformity, error)
|
||||||
GetStandard(ctx *fiber.Ctx, uniformity *entity.ProjectFlockKandangUniformity) (*UniformityStandard, error)
|
GetStandard(ctx *fiber.Ctx, uniformity *entity.ProjectFlockKandangUniformity) (*utypes.UniformityStandard, error)
|
||||||
MapStandards(ctx *fiber.Ctx, items []entity.ProjectFlockKandangUniformity) (map[uint]UniformityStandard, error)
|
MapStandards(ctx *fiber.Ctx, items []entity.ProjectFlockKandangUniformity) (map[uint]utypes.UniformityStandard, error)
|
||||||
|
MapCharts(ctx *fiber.Ctx, items []entity.ProjectFlockKandangUniformity) (map[uint]utypes.UniformityChartData, error)
|
||||||
MapDocuments(ctx *fiber.Ctx, items []entity.ProjectFlockKandangUniformity) (map[uint]string, error)
|
MapDocuments(ctx *fiber.Ctx, items []entity.ProjectFlockKandangUniformity) (map[uint]string, error)
|
||||||
CreateOne(ctx *fiber.Ctx, req *validation.Create, file *multipart.FileHeader, rows []BodyWeightExcelRow) (*entity.ProjectFlockKandangUniformity, error)
|
CreateOne(ctx *fiber.Ctx, req *validation.Create, file *multipart.FileHeader, rows []BodyWeightExcelRow) (*entity.ProjectFlockKandangUniformity, error)
|
||||||
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint, file *multipart.FileHeader, rows []BodyWeightExcelRow) (*entity.ProjectFlockKandangUniformity, error)
|
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint, file *multipart.FileHeader, rows []BodyWeightExcelRow) (*entity.ProjectFlockKandangUniformity, error)
|
||||||
DeleteOne(ctx *fiber.Ctx, id uint) error
|
DeleteOne(ctx *fiber.Ctx, id uint) error
|
||||||
Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.ProjectFlockKandangUniformity, error)
|
Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.ProjectFlockKandangUniformity, error)
|
||||||
ParseBodyWeightExcel(ctx *fiber.Ctx, file *multipart.FileHeader) ([]BodyWeightExcelRow, error)
|
ParseBodyWeightExcel(ctx *fiber.Ctx, file *multipart.FileHeader) ([]BodyWeightExcelRow, error)
|
||||||
ComputeUniformity(rows []BodyWeightExcelRow) (UniformityCalculation, error)
|
ComputeUniformity(rows []BodyWeightExcelRow) (utypes.UniformityCalculation, error)
|
||||||
GetDocumentInfo(ctx *fiber.Ctx, uniformityID uint) (*entity.Document, string, error)
|
GetDocumentInfo(ctx *fiber.Ctx, uniformityID uint) (*entity.Document, string, error)
|
||||||
CalculateUniformityFromDocument(ctx *fiber.Ctx, uniformityID uint) (UniformityCalculation, *entity.Document, string, error)
|
CalculateUniformityFromDocument(ctx *fiber.Ctx, uniformityID uint) (utypes.UniformityCalculation, *entity.Document, string, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type uniformityService struct {
|
type uniformityService struct {
|
||||||
@@ -79,29 +82,13 @@ func NewUniformityService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s uniformityService) withRelations(db *gorm.DB) *gorm.DB {
|
|
||||||
return db.
|
|
||||||
Preload("ProjectFlockKandang.ProjectFlock.Location").
|
|
||||||
Preload("ProjectFlockKandang.Kandang.Location")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s uniformityService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlockKandangUniformity, int64, error) {
|
func (s uniformityService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlockKandangUniformity, int64, error) {
|
||||||
if err := s.Validate.Struct(params); err != nil {
|
if err := s.Validate.Struct(params); err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
offset := (params.Page - 1) * params.Limit
|
offset := (params.Page - 1) * params.Limit
|
||||||
|
uniformitys, total, err := s.Repository.GetAllWithFilters(c.Context(), offset, params.Limit, params)
|
||||||
uniformitys, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
|
||||||
db = s.withRelations(db)
|
|
||||||
if params.ProjectFlockKandangId != 0 {
|
|
||||||
db = db.Where("project_flock_kandang_id = ?", params.ProjectFlockKandangId)
|
|
||||||
}
|
|
||||||
if params.Week != 0 {
|
|
||||||
db = db.Where("week = ?", params.Week)
|
|
||||||
}
|
|
||||||
return db.Order("uniform_date DESC").Order("id DESC")
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log.Errorf("Failed to get uniformitys: %+v", err)
|
s.Log.Errorf("Failed to get uniformitys: %+v", err)
|
||||||
@@ -114,7 +101,7 @@ func (s uniformityService) GetAll(c *fiber.Ctx, params *validation.Query) ([]ent
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s uniformityService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectFlockKandangUniformity, error) {
|
func (s uniformityService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectFlockKandangUniformity, error) {
|
||||||
uniformity, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
|
uniformity, err := s.Repository.GetByID(c.Context(), id, s.Repository.WithDefaultRelations())
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return nil, fiber.NewError(fiber.StatusNotFound, "Uniformity not found")
|
return nil, fiber.NewError(fiber.StatusNotFound, "Uniformity not found")
|
||||||
}
|
}
|
||||||
@@ -132,14 +119,14 @@ func (s uniformityService) GetSummary(c *fiber.Ctx, id uint) (*entity.ProjectFlo
|
|||||||
return s.GetOne(c, id)
|
return s.GetOne(c, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s uniformityService) GetStandard(c *fiber.Ctx, uniformity *entity.ProjectFlockKandangUniformity) (*UniformityStandard, error) {
|
func (s uniformityService) GetStandard(c *fiber.Ctx, uniformity *entity.ProjectFlockKandangUniformity) (*utypes.UniformityStandard, error) {
|
||||||
if uniformity == nil {
|
if uniformity == nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
return s.resolveUniformityStandard(c.Context(), *uniformity)
|
return s.resolveUniformityStandard(c.Context(), *uniformity)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s uniformityService) MapStandards(c *fiber.Ctx, items []entity.ProjectFlockKandangUniformity) (map[uint]UniformityStandard, error) {
|
func (s uniformityService) MapStandards(c *fiber.Ctx, items []entity.ProjectFlockKandangUniformity) (map[uint]utypes.UniformityStandard, error) {
|
||||||
if len(items) == 0 {
|
if len(items) == 0 {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
@@ -149,7 +136,7 @@ func (s uniformityService) MapStandards(c *fiber.Ctx, items []entity.ProjectFloc
|
|||||||
|
|
||||||
categoryStandard := make(map[string]*entity.ProductionStandard)
|
categoryStandard := make(map[string]*entity.ProductionStandard)
|
||||||
detailCache := make(map[uint]map[int]entity.StandardGrowthDetail)
|
detailCache := make(map[uint]map[int]entity.StandardGrowthDetail)
|
||||||
result := make(map[uint]UniformityStandard, len(items))
|
result := make(map[uint]utypes.UniformityStandard, len(items))
|
||||||
|
|
||||||
for _, item := range items {
|
for _, item := range items {
|
||||||
if item.Id == 0 {
|
if item.Id == 0 {
|
||||||
@@ -180,7 +167,7 @@ func (s uniformityService) MapStandards(c *fiber.Ctx, items []entity.ProjectFloc
|
|||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
standardDTO := UniformityStandard{
|
standardDTO := utypes.UniformityStandard{
|
||||||
MeanWeight: cloneFloat64(detail.TargetMeanBw),
|
MeanWeight: cloneFloat64(detail.TargetMeanBw),
|
||||||
Uniformity: float64Ptr(detail.MinUniformity),
|
Uniformity: float64Ptr(detail.MinUniformity),
|
||||||
}
|
}
|
||||||
@@ -190,6 +177,109 @@ func (s uniformityService) MapStandards(c *fiber.Ctx, items []entity.ProjectFloc
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s uniformityService) MapCharts(c *fiber.Ctx, items []entity.ProjectFlockKandangUniformity) (map[uint]utypes.UniformityChartData, error) {
|
||||||
|
if len(items) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
grouped := make(map[uint][]entity.ProjectFlockKandangUniformity)
|
||||||
|
for _, item := range items {
|
||||||
|
if item.ProjectFlockKandangId == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
grouped[item.ProjectFlockKandangId] = append(grouped[item.ProjectFlockKandangId], item)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(grouped) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make(map[uint]utypes.UniformityChartData, len(items))
|
||||||
|
|
||||||
|
for _, group := range grouped {
|
||||||
|
allWeeks := make(map[int]utypes.UniformityChartWeek)
|
||||||
|
weekOrder := make([]int, 0, len(group))
|
||||||
|
weekSeen := make(map[int]struct{}, len(group))
|
||||||
|
weeksWithData := 0
|
||||||
|
gaugeWeeks := make([]utypes.UniformityChartGaugeWeek, 0, len(group))
|
||||||
|
|
||||||
|
for _, item := range group {
|
||||||
|
if item.Week == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var weekSummary utypes.UniformityChartWeek
|
||||||
|
if len(item.ChartData) > 0 {
|
||||||
|
if err := json.Unmarshal(item.ChartData, &weekSummary); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if weekSummary.WeightDistribution == nil {
|
||||||
|
weekSummary.WeightDistribution = []utypes.UniformityChartRange{}
|
||||||
|
}
|
||||||
|
if !weekSummary.HasData && item.ChickQtyOfWeight > 0 {
|
||||||
|
weekSummary.HasData = true
|
||||||
|
}
|
||||||
|
if weekSummary.HasData {
|
||||||
|
weeksWithData++
|
||||||
|
}
|
||||||
|
allWeeks[item.Week] = weekSummary
|
||||||
|
|
||||||
|
if _, ok := weekSeen[item.Week]; !ok {
|
||||||
|
weekSeen[item.Week] = struct{}{}
|
||||||
|
weekOrder = append(weekOrder, item.Week)
|
||||||
|
}
|
||||||
|
|
||||||
|
hasData := item.ChickQtyOfWeight > 0
|
||||||
|
gaugeWeeks = append(gaugeWeeks, utypes.UniformityChartGaugeWeek{
|
||||||
|
Week: item.Week,
|
||||||
|
UniformityPercent: item.Uniformity,
|
||||||
|
IdealCount: item.UniformQty,
|
||||||
|
OutsideIdealCount: item.NotUniformQty,
|
||||||
|
TotalCount: item.ChickQtyOfWeight,
|
||||||
|
HasData: hasData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Ints(weekOrder)
|
||||||
|
sort.Slice(gaugeWeeks, func(i, j int) bool {
|
||||||
|
return gaugeWeeks[i].Week < gaugeWeeks[j].Week
|
||||||
|
})
|
||||||
|
|
||||||
|
weekIndex := make(map[int]int, len(weekOrder))
|
||||||
|
for idx, week := range weekOrder {
|
||||||
|
weekIndex[week] = idx
|
||||||
|
}
|
||||||
|
|
||||||
|
totalWeeks := len(weekOrder)
|
||||||
|
for _, item := range group {
|
||||||
|
if item.Id == 0 || item.Week == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
currentIndex := weekIndex[item.Week]
|
||||||
|
chart := utypes.UniformityChartData{
|
||||||
|
BarChart: utypes.UniformityChartBar{
|
||||||
|
CurrentWeek: item.Week,
|
||||||
|
AllWeeks: allWeeks,
|
||||||
|
},
|
||||||
|
GaugeChart: utypes.UniformityChartGauge{
|
||||||
|
CurrentWeek: item.Week,
|
||||||
|
AvailableWeeks: gaugeWeeks,
|
||||||
|
WeekInfo: utypes.UniformityChartWeekInfo{
|
||||||
|
TotalWeeks: totalWeeks,
|
||||||
|
WeeksWithData: weeksWithData,
|
||||||
|
CurrentWeekIndex: currentIndex,
|
||||||
|
HasPrevWeek: currentIndex > 0,
|
||||||
|
HasNextWeek: currentIndex < totalWeeks-1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
result[item.Id] = chart
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s uniformityService) MapDocuments(c *fiber.Ctx, items []entity.ProjectFlockKandangUniformity) (map[uint]string, error) {
|
func (s uniformityService) MapDocuments(c *fiber.Ctx, items []entity.ProjectFlockKandangUniformity) (map[uint]string, error) {
|
||||||
if s.DocumentSvc == nil || len(items) == 0 {
|
if s.DocumentSvc == nil || len(items) == 0 {
|
||||||
return map[uint]string{}, nil
|
return map[uint]string{}, nil
|
||||||
@@ -252,6 +342,11 @@ func (s *uniformityService) CreateOne(c *fiber.Ctx, req *validation.Create, file
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
chartSummary := buildChartWeekSummary(extractWeights(rows))
|
||||||
|
chartJSON, err := json.Marshal(chartSummary)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "failed to build chart data")
|
||||||
|
}
|
||||||
actorID, err := m.ActorIDFromContext(c)
|
actorID, err := m.ActorIDFromContext(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -267,6 +362,7 @@ func (s *uniformityService) CreateOne(c *fiber.Ctx, req *validation.Create, file
|
|||||||
ProjectFlockKandangId: req.ProjectFlockKandangId,
|
ProjectFlockKandangId: req.ProjectFlockKandangId,
|
||||||
UniformQty: calculation.UniformQty,
|
UniformQty: calculation.UniformQty,
|
||||||
NotUniformQty: calculation.OutsideQty,
|
NotUniformQty: calculation.OutsideQty,
|
||||||
|
ChartData: chartJSON,
|
||||||
UniformDate: &uniformDate,
|
UniformDate: &uniformDate,
|
||||||
CreatedBy: actorID,
|
CreatedBy: actorID,
|
||||||
}
|
}
|
||||||
@@ -307,7 +403,12 @@ func (s *uniformityService) CreateOne(c *fiber.Ctx, req *validation.Create, file
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.rollbackUniformityCreate(c.Context(), createBody.Id)
|
if errDelete := s.ApprovalRepo.DeleteByTarget(c.Context(), utils.ApprovalWorkflowUniformity.String(), createBody.Id); errDelete != nil {
|
||||||
|
s.Log.WithError(errDelete).Warnf("Failed to rollback uniformity approvals for %d", createBody.Id)
|
||||||
|
}
|
||||||
|
if errDelete := s.Repository.DeleteOne(c.Context(), createBody.Id); errDelete != nil {
|
||||||
|
s.Log.WithError(errDelete).Warnf("Failed to rollback uniformity %d", createBody.Id)
|
||||||
|
}
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to upload uniformity document")
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to upload uniformity document")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -391,6 +492,11 @@ func (s uniformityService) UpdateOne(c *fiber.Ctx, req *validation.Update, id ui
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
chartSummary := buildChartWeekSummary(extractWeights(rows))
|
||||||
|
chartJSON, err := json.Marshal(chartSummary)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "failed to build chart data")
|
||||||
|
}
|
||||||
|
|
||||||
updateBody["uniformity"] = calculation.Uniformity
|
updateBody["uniformity"] = calculation.Uniformity
|
||||||
updateBody["cv"] = calculation.Cv
|
updateBody["cv"] = calculation.Cv
|
||||||
@@ -399,6 +505,7 @@ func (s uniformityService) UpdateOne(c *fiber.Ctx, req *validation.Update, id ui
|
|||||||
updateBody["mean_down"] = calculation.MeanDown
|
updateBody["mean_down"] = calculation.MeanDown
|
||||||
updateBody["uniform_qty"] = calculation.UniformQty
|
updateBody["uniform_qty"] = calculation.UniformQty
|
||||||
updateBody["not_uniform_qty"] = calculation.OutsideQty
|
updateBody["not_uniform_qty"] = calculation.OutsideQty
|
||||||
|
updateBody["chart_data"] = chartJSON
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(updateBody) == 0 {
|
if len(updateBody) == 0 {
|
||||||
@@ -590,30 +697,7 @@ func (s uniformityService) Approval(c *fiber.Ctx, req *validation.Approve) ([]en
|
|||||||
return results, nil
|
return results, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type UniformityDetailItem struct {
|
func (s uniformityService) ComputeUniformity(rows []BodyWeightExcelRow) (utypes.UniformityCalculation, error) {
|
||||||
Id int
|
|
||||||
Weight float64
|
|
||||||
Range string
|
|
||||||
}
|
|
||||||
|
|
||||||
type UniformityCalculation struct {
|
|
||||||
ChickQtyOfWeight float64
|
|
||||||
MeanWeight float64
|
|
||||||
MeanDown float64
|
|
||||||
MeanUp float64
|
|
||||||
UniformQty float64
|
|
||||||
OutsideQty float64
|
|
||||||
Uniformity float64
|
|
||||||
Cv float64
|
|
||||||
Details []UniformityDetailItem
|
|
||||||
}
|
|
||||||
|
|
||||||
type UniformityStandard struct {
|
|
||||||
MeanWeight *float64
|
|
||||||
Uniformity *float64
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s uniformityService) ComputeUniformity(rows []BodyWeightExcelRow) (UniformityCalculation, error) {
|
|
||||||
return computeUniformity(rows)
|
return computeUniformity(rows)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -621,37 +705,37 @@ func (s uniformityService) GetDocumentInfo(c *fiber.Ctx, uniformityID uint) (*en
|
|||||||
return s.fetchUniformityDocument(c.Context(), uniformityID, true)
|
return s.fetchUniformityDocument(c.Context(), uniformityID, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s uniformityService) CalculateUniformityFromDocument(c *fiber.Ctx, uniformityID uint) (UniformityCalculation, *entity.Document, string, error) {
|
func (s uniformityService) CalculateUniformityFromDocument(c *fiber.Ctx, uniformityID uint) (utypes.UniformityCalculation, *entity.Document, string, error) {
|
||||||
document, url, err := s.fetchUniformityDocument(c.Context(), uniformityID, false)
|
document, url, err := s.fetchUniformityDocument(c.Context(), uniformityID, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return UniformityCalculation{}, nil, "", err
|
return utypes.UniformityCalculation{}, nil, "", err
|
||||||
}
|
}
|
||||||
if document == nil || url == "" {
|
if document == nil || url == "" {
|
||||||
return UniformityCalculation{}, nil, "", fiber.NewError(fiber.StatusNotFound, "Uniformity document not found")
|
return utypes.UniformityCalculation{}, nil, "", fiber.NewError(fiber.StatusNotFound, "Uniformity document not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(c.Context(), http.MethodGet, url, nil)
|
req, err := http.NewRequestWithContext(c.Context(), http.MethodGet, url, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return UniformityCalculation{}, nil, "", err
|
return utypes.UniformityCalculation{}, nil, "", err
|
||||||
}
|
}
|
||||||
resp, err := http.DefaultClient.Do(req)
|
resp, err := http.DefaultClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return UniformityCalculation{}, nil, "", err
|
return utypes.UniformityCalculation{}, nil, "", err
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||||
return UniformityCalculation{}, nil, "", fiber.NewError(fiber.StatusBadRequest, "Failed to download uniformity document")
|
return utypes.UniformityCalculation{}, nil, "", fiber.NewError(fiber.StatusBadRequest, "Failed to download uniformity document")
|
||||||
}
|
}
|
||||||
|
|
||||||
rows, err := parseBodyWeightExcelReader(resp.Body)
|
rows, err := parseBodyWeightExcelReader(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return UniformityCalculation{}, nil, "", err
|
return utypes.UniformityCalculation{}, nil, "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
calculation, err := computeUniformity(rows)
|
calculation, err := computeUniformity(rows)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return UniformityCalculation{}, nil, "", err
|
return utypes.UniformityCalculation{}, nil, "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
return calculation, document, url, nil
|
return calculation, document, url, nil
|
||||||
@@ -783,7 +867,7 @@ func (s *uniformityService) attachLatestApproval(ctx context.Context, item *enti
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *uniformityService) resolveUniformityStandard(ctx context.Context, item entity.ProjectFlockKandangUniformity) (*UniformityStandard, error) {
|
func (s *uniformityService) resolveUniformityStandard(ctx context.Context, item entity.ProjectFlockKandangUniformity) (*utypes.UniformityStandard, error) {
|
||||||
if s.ProductionStandardRepo == nil || s.StandardGrowthDetailRepo == nil {
|
if s.ProductionStandardRepo == nil || s.StandardGrowthDetailRepo == nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
@@ -801,7 +885,7 @@ func (s *uniformityService) resolveUniformityStandard(ctx context.Context, item
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &UniformityStandard{
|
return &utypes.UniformityStandard{
|
||||||
MeanWeight: cloneFloat64(detail.TargetMeanBw),
|
MeanWeight: cloneFloat64(detail.TargetMeanBw),
|
||||||
Uniformity: float64Ptr(detail.MinUniformity),
|
Uniformity: float64Ptr(detail.MinUniformity),
|
||||||
}, nil
|
}, nil
|
||||||
@@ -858,22 +942,6 @@ func float64Ptr(value float64) *float64 {
|
|||||||
return ©
|
return ©
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *uniformityService) rollbackUniformityCreate(ctx context.Context, uniformityID uint) {
|
|
||||||
if uniformityID == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.ApprovalRepo != nil {
|
|
||||||
if err := s.ApprovalRepo.DeleteByTarget(ctx, utils.ApprovalWorkflowUniformity.String(), uniformityID); err != nil {
|
|
||||||
s.Log.WithError(err).Warnf("Failed to rollback uniformity approvals for %d", uniformityID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.Repository.DeleteOne(ctx, uniformityID); err != nil {
|
|
||||||
s.Log.WithError(err).Warnf("Failed to rollback uniformity %d", uniformityID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func uniqueUintSlice(values []uint) []uint {
|
func uniqueUintSlice(values []uint) []uint {
|
||||||
if len(values) == 0 {
|
if len(values) == 0 {
|
||||||
return nil
|
return nil
|
||||||
@@ -893,114 +961,3 @@ func uniqueUintSlice(values []uint) []uint {
|
|||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func computeUniformity(rows []BodyWeightExcelRow) (UniformityCalculation, error) {
|
|
||||||
weights := make([]float64, 0, len(rows))
|
|
||||||
details := make([]UniformityDetailItem, 0, len(rows))
|
|
||||||
hasRangeLabels := false
|
|
||||||
for idx, row := range rows {
|
|
||||||
if row.Weight <= 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
id := row.No
|
|
||||||
if id <= 0 {
|
|
||||||
id = idx + 1
|
|
||||||
}
|
|
||||||
weights = append(weights, row.Weight)
|
|
||||||
rangeLabel := strings.TrimSpace(row.Range)
|
|
||||||
if rangeLabel != "" {
|
|
||||||
upper := strings.ToUpper(rangeLabel)
|
|
||||||
if upper == "HIGH" || upper == "LOW" {
|
|
||||||
hasRangeLabels = true
|
|
||||||
}
|
|
||||||
rangeLabel = upper
|
|
||||||
}
|
|
||||||
details = append(details, UniformityDetailItem{
|
|
||||||
Id: id,
|
|
||||||
Weight: row.Weight,
|
|
||||||
Range: rangeLabel,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
total := float64(len(weights))
|
|
||||||
if total == 0 {
|
|
||||||
return UniformityCalculation{}, fiber.NewError(fiber.StatusBadRequest, "no body weight data found")
|
|
||||||
}
|
|
||||||
|
|
||||||
var sum float64
|
|
||||||
for _, w := range weights {
|
|
||||||
sum += w
|
|
||||||
}
|
|
||||||
mean := sum / total
|
|
||||||
meanUpThreshold := roundToPrecision(mean*1.10, 3)
|
|
||||||
meanDownThreshold := roundToPrecision(mean*0.90, 3)
|
|
||||||
|
|
||||||
var uniformCount float64
|
|
||||||
for i := range details {
|
|
||||||
if hasRangeLabels {
|
|
||||||
if details[i].Range == "HIGH" || details[i].Range == "LOW" {
|
|
||||||
details[i].Range = "Outside"
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
details[i].Range = "Ideal"
|
|
||||||
uniformCount++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
w := details[i].Weight
|
|
||||||
if w > meanUpThreshold || w < meanDownThreshold {
|
|
||||||
details[i].Range = "Outside"
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
details[i].Range = "Ideal"
|
|
||||||
uniformCount++
|
|
||||||
}
|
|
||||||
outsideCount := total - uniformCount
|
|
||||||
|
|
||||||
var cv float64
|
|
||||||
if mean > 0 && total > 1 {
|
|
||||||
stddevWeights := weights
|
|
||||||
stddevCount := float64(len(stddevWeights))
|
|
||||||
if stddevCount > 1 {
|
|
||||||
var stddevSum float64
|
|
||||||
for _, w := range stddevWeights {
|
|
||||||
stddevSum += w
|
|
||||||
}
|
|
||||||
stddevMean := stddevSum / stddevCount
|
|
||||||
var sumSquares float64
|
|
||||||
for _, w := range stddevWeights {
|
|
||||||
diff := w - stddevMean
|
|
||||||
sumSquares += diff * diff
|
|
||||||
}
|
|
||||||
stddev := math.Sqrt(sumSquares / (stddevCount - 1))
|
|
||||||
cv = (stddev / mean) * 100
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
uniformity := (uniformCount / total) * 100
|
|
||||||
|
|
||||||
return UniformityCalculation{
|
|
||||||
ChickQtyOfWeight: total,
|
|
||||||
MeanWeight: roundToPrecision(mean, 0),
|
|
||||||
MeanDown: roundToPrecision(mean*0.90, 0),
|
|
||||||
MeanUp: roundToPrecision(mean*1.10, 0),
|
|
||||||
UniformQty: uniformCount,
|
|
||||||
OutsideQty: outsideCount,
|
|
||||||
Uniformity: roundToPrecision(uniformity, 0),
|
|
||||||
Cv: roundToPrecision(cv, 1),
|
|
||||||
Details: details,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func roundToPrecision(value float64, precision int) float64 {
|
|
||||||
if precision < 0 {
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
scale := math.Pow10(precision)
|
|
||||||
scaled := value * scale
|
|
||||||
fraction := scaled - math.Floor(scaled)
|
|
||||||
if fraction >= 0.5 {
|
|
||||||
return math.Ceil(scaled) / scale
|
|
||||||
}
|
|
||||||
return math.Floor(scaled) / scale
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
type UniformityDetailItem struct {
|
||||||
|
Id int
|
||||||
|
Weight float64
|
||||||
|
Range string
|
||||||
|
}
|
||||||
|
|
||||||
|
type UniformityCalculation struct {
|
||||||
|
ChickQtyOfWeight float64
|
||||||
|
MeanWeight float64
|
||||||
|
MeanDown float64
|
||||||
|
MeanUp float64
|
||||||
|
UniformQty float64
|
||||||
|
OutsideQty float64
|
||||||
|
Uniformity float64
|
||||||
|
Cv float64
|
||||||
|
Details []UniformityDetailItem
|
||||||
|
}
|
||||||
|
|
||||||
|
type UniformityStandard struct {
|
||||||
|
MeanWeight *float64
|
||||||
|
Uniformity *float64
|
||||||
|
}
|
||||||
|
|
||||||
|
type UniformityChartRange struct {
|
||||||
|
Range string `json:"range"`
|
||||||
|
MinWeight float64 `json:"min_weight"`
|
||||||
|
MaxWeight float64 `json:"max_weight"`
|
||||||
|
BirdCount float64 `json:"bird_count"`
|
||||||
|
IsIdealRange bool `json:"is_ideal_range"`
|
||||||
|
IdealRange string `json:"ideal_range,omitempty"`
|
||||||
|
OutsideRange string `json:"outside_range,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UniformityChartIdealRange struct {
|
||||||
|
MinWeight float64 `json:"min_weight"`
|
||||||
|
MaxWeight float64 `json:"max_weight"`
|
||||||
|
TotalIdealBirds float64 `json:"total_ideal_birds"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UniformityChartStatistics struct {
|
||||||
|
MinWeight float64 `json:"min_weight"`
|
||||||
|
MaxWeight float64 `json:"max_weight"`
|
||||||
|
AverageWeight float64 `json:"average_weight"`
|
||||||
|
TotalBirdsMeasured float64 `json:"total_birds_measured"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UniformityChartWeek struct {
|
||||||
|
HasData bool `json:"has_data"`
|
||||||
|
WeightDistribution []UniformityChartRange `json:"weight_distribution"`
|
||||||
|
IdealRange *UniformityChartIdealRange `json:"ideal_range"`
|
||||||
|
Statistics *UniformityChartStatistics `json:"statistics"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UniformityChartBar struct {
|
||||||
|
CurrentWeek int `json:"current_week"`
|
||||||
|
AllWeeks map[int]UniformityChartWeek `json:"all_weeks"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UniformityChartGaugeWeek struct {
|
||||||
|
Week int `json:"week"`
|
||||||
|
UniformityPercent float64 `json:"uniformity_percentage"`
|
||||||
|
IdealCount float64 `json:"ideal_count"`
|
||||||
|
OutsideIdealCount float64 `json:"outside_ideal_count"`
|
||||||
|
TotalCount float64 `json:"total_count"`
|
||||||
|
HasData bool `json:"has_data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UniformityChartWeekInfo struct {
|
||||||
|
TotalWeeks int `json:"total_weeks"`
|
||||||
|
WeeksWithData int `json:"weeks_with_data"`
|
||||||
|
CurrentWeekIndex int `json:"current_week_index"`
|
||||||
|
HasPrevWeek bool `json:"has_prev_week"`
|
||||||
|
HasNextWeek bool `json:"has_next_week"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UniformityChartGauge struct {
|
||||||
|
CurrentWeek int `json:"current_week"`
|
||||||
|
AvailableWeeks []UniformityChartGaugeWeek `json:"available_weeks"`
|
||||||
|
WeekInfo UniformityChartWeekInfo `json:"week_info"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UniformityChartData struct {
|
||||||
|
BarChart UniformityChartBar `json:"bar_chart"`
|
||||||
|
GaugeChart UniformityChartGauge `json:"gauge_chart"`
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
)
|
)
|
||||||
@@ -21,10 +22,13 @@ type Update struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Query struct {
|
type Query struct {
|
||||||
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
|
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
|
||||||
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
|
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
|
||||||
ProjectFlockKandangId uint `query:"project_flock_kandang_id" validate:"omitempty,number,min=1"`
|
ProjectFlockKandangId uint `query:"project_flock_kandang_id" validate:"omitempty,number,min=1"`
|
||||||
Week int `query:"week" validate:"omitempty,min=1"`
|
Week int `query:"week" validate:"omitempty,min=1"`
|
||||||
|
StartDate string `query:"start_date" validate:"omitempty"`
|
||||||
|
EndDate string `query:"end_date" validate:"omitempty"`
|
||||||
|
WithChart bool `query:"with_chart"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UploadExcelRequest struct {
|
type UploadExcelRequest struct {
|
||||||
@@ -37,6 +41,8 @@ type Approve struct {
|
|||||||
Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"`
|
Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const maxUniformityUploadBytes = 5 * 1024 * 1024
|
||||||
|
|
||||||
func ParseIDParam(c *fiber.Ctx, name string) (uint, error) {
|
func ParseIDParam(c *fiber.Ctx, name string) (uint, error) {
|
||||||
raw := strings.TrimSpace(c.Params(name))
|
raw := strings.TrimSpace(c.Params(name))
|
||||||
if raw == "" {
|
if raw == "" {
|
||||||
@@ -55,15 +61,49 @@ func ParseQuery(c *fiber.Ctx) (*Query, error) {
|
|||||||
Limit: c.QueryInt("limit", 10),
|
Limit: c.QueryInt("limit", 10),
|
||||||
ProjectFlockKandangId: uint(c.QueryInt("project_flock_kandang_id", 0)),
|
ProjectFlockKandangId: uint(c.QueryInt("project_flock_kandang_id", 0)),
|
||||||
Week: c.QueryInt("week", 0),
|
Week: c.QueryInt("week", 0),
|
||||||
|
StartDate: strings.TrimSpace(c.Query("start_date")),
|
||||||
|
EndDate: strings.TrimSpace(c.Query("end_date")),
|
||||||
|
WithChart: c.QueryBool("with_chart", false),
|
||||||
}
|
}
|
||||||
|
|
||||||
if query.Page < 1 || query.Limit < 1 {
|
if query.Page < 1 || query.Limit < 1 {
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
|
return nil, fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if _, _, err := ParseDateRange(query.StartDate, query.EndDate); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
return query, nil
|
return query, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ParseDateRange(startDate, endDate string) (*time.Time, *time.Time, error) {
|
||||||
|
var startDateValue *time.Time
|
||||||
|
var endDateValue *time.Time
|
||||||
|
|
||||||
|
if startDate != "" {
|
||||||
|
parsed, err := time.Parse("2006-01-02", startDate)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fiber.NewError(fiber.StatusBadRequest, "start_date must be in YYYY-MM-DD format")
|
||||||
|
}
|
||||||
|
startDateValue = &parsed
|
||||||
|
}
|
||||||
|
if endDate != "" {
|
||||||
|
parsed, err := time.Parse("2006-01-02", endDate)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fiber.NewError(fiber.StatusBadRequest, "end_date must be in YYYY-MM-DD format")
|
||||||
|
}
|
||||||
|
endDateValue = &parsed
|
||||||
|
}
|
||||||
|
if startDateValue != nil && endDateValue != nil {
|
||||||
|
if endDateValue.Before(*startDateValue) {
|
||||||
|
return nil, nil, fiber.NewError(fiber.StatusBadRequest, "end_date must be greater than or equal to start_date")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return startDateValue, endDateValue, nil
|
||||||
|
}
|
||||||
|
|
||||||
func ParseCreate(c *fiber.Ctx) (*Create, *multipart.FileHeader, error) {
|
func ParseCreate(c *fiber.Ctx) (*Create, *multipart.FileHeader, error) {
|
||||||
date := strings.TrimSpace(c.FormValue("date"))
|
date := strings.TrimSpace(c.FormValue("date"))
|
||||||
if date == "" {
|
if date == "" {
|
||||||
@@ -94,6 +134,9 @@ func ParseCreate(c *fiber.Ctx) (*Create, *multipart.FileHeader, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, fiber.NewError(fiber.StatusBadRequest, "document is required")
|
return nil, nil, fiber.NewError(fiber.StatusBadRequest, "document is required")
|
||||||
}
|
}
|
||||||
|
if err := validateUniformityFileSize(file); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
return &Create{
|
return &Create{
|
||||||
Date: date,
|
Date: date,
|
||||||
@@ -134,6 +177,8 @@ func ParseUpdate(c *fiber.Ctx) (*Update, *multipart.FileHeader, error) {
|
|||||||
file, err := c.FormFile("document")
|
file, err := c.FormFile("document")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
file = nil
|
file = nil
|
||||||
|
} else if err := validateUniformityFileSize(file); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return req, file, nil
|
return req, file, nil
|
||||||
@@ -151,6 +196,9 @@ func ParseUploadFiles(c *fiber.Ctx) ([]*multipart.FileHeader, error) {
|
|||||||
if err != nil || file == nil {
|
if err != nil || file == nil {
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "document is required")
|
return nil, fiber.NewError(fiber.StatusBadRequest, "document is required")
|
||||||
}
|
}
|
||||||
|
if err := validateUniformityFileSize(file); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
return []*multipart.FileHeader{file}, nil
|
return []*multipart.FileHeader{file}, nil
|
||||||
}
|
}
|
||||||
@@ -162,3 +210,10 @@ func ParseApprove(c *fiber.Ctx) (*Approve, error) {
|
|||||||
}
|
}
|
||||||
return req, nil
|
return req, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validateUniformityFileSize(file *multipart.FileHeader) error {
|
||||||
|
if file != nil && file.Size > maxUniformityUploadBytes {
|
||||||
|
return fiber.NewError(fiber.StatusRequestEntityTooLarge, "Document size must be <= 5MB")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
|
"mime/multipart"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -15,6 +16,8 @@ import (
|
|||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const maxPurchaseUploadBytes = 5 * 1024 * 1024
|
||||||
|
|
||||||
type PurchaseController struct {
|
type PurchaseController struct {
|
||||||
service service.PurchaseService
|
service service.PurchaseService
|
||||||
}
|
}
|
||||||
@@ -184,6 +187,9 @@ func (ctrl *PurchaseController) ReceiveProducts(c *fiber.Ctx) error {
|
|||||||
if len(req.TravelDocuments) == 0 {
|
if len(req.TravelDocuments) == 0 {
|
||||||
req.TravelDocuments = form.File["documents"]
|
req.TravelDocuments = form.File["documents"]
|
||||||
}
|
}
|
||||||
|
if err := validatePurchaseDocumentSizes(req.TravelDocuments); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
result, err := ctrl.service.ReceiveProducts(c, uint(id), req)
|
result, err := ctrl.service.ReceiveProducts(c, uint(id), req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -198,6 +204,15 @@ func (ctrl *PurchaseController) ReceiveProducts(c *fiber.Ctx) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validatePurchaseDocumentSizes(files []*multipart.FileHeader) error {
|
||||||
|
for _, file := range files {
|
||||||
|
if file != nil && file.Size > maxPurchaseUploadBytes {
|
||||||
|
return fiber.NewError(fiber.StatusRequestEntityTooLarge, "Document size must be <= 5MB")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (ctrl *PurchaseController) DeleteItems(c *fiber.Ctx) error {
|
func (ctrl *PurchaseController) DeleteItems(c *fiber.Ctx) error {
|
||||||
param := c.Params("id")
|
param := c.Params("id")
|
||||||
id, err := strconv.Atoi(param)
|
id, err := strconv.Atoi(param)
|
||||||
|
|||||||
Reference in New Issue
Block a user