Merge branch 'development' of https://gitlab.com/mbugroup/lti-api into dev/teguh

This commit is contained in:
aguhh18
2026-01-07 14:03:08 +07:00
66 changed files with 3619 additions and 40 deletions
@@ -134,7 +134,14 @@ func ToSapronakProjectAggregatedFromReport(report *SapronakReportDTO, flag strin
report = &SapronakReportDTO{}
}
filter := strings.ToUpper(strings.TrimSpace(flag))
normalizeFlag := func(raw string) string {
normalized := strings.ToUpper(strings.TrimSpace(raw))
if normalized == "PULLET" {
return "DOC"
}
return normalized
}
filter := normalizeFlag(flag)
byFlag := map[string]**SapronakCategoryDTO{}
if filter == "" || filter == "DOC" {
@@ -149,10 +156,6 @@ func ToSapronakProjectAggregatedFromReport(report *SapronakReportDTO, flag strin
result.Pakan = &SapronakCategoryDTO{Rows: make([]SapronakCategoryRowDTO, 0)}
byFlag["PAKAN"] = &result.Pakan
}
if filter == "" || filter == "PULLET" {
result.Pullet = &SapronakCategoryDTO{Rows: make([]SapronakCategoryRowDTO, 0)}
byFlag["PULLET"] = &result.Pullet
}
formatDate := func(t *time.Time) string {
if t == nil {
@@ -162,7 +165,7 @@ func ToSapronakProjectAggregatedFromReport(report *SapronakReportDTO, flag strin
}
for _, group := range report.Groups {
flagKey := strings.ToUpper(group.Flag)
flagKey := normalizeFlag(group.Flag)
ptr := byFlag[flagKey]
if ptr == nil || *ptr == nil {
continue
@@ -182,7 +185,7 @@ func ToSapronakProjectAggregatedFromReport(report *SapronakReportDTO, flag strin
}
for idx, item := range group.Items {
productKey := strings.ToUpper(group.Flag + "|" + item.ProductName)
productKey := strings.ToUpper(flagKey + "|" + item.ProductName)
baseRow := SapronakCategoryRowDTO{
ID: idx + 1,
Date: formatDate(item.Tanggal),
@@ -246,7 +249,5 @@ func ToSapronakProjectAggregatedFromReport(report *SapronakReportDTO, flag strin
buildTotals(result.Doc, "TOTAL DOC")
buildTotals(result.Ovk, "TOTAL OVK")
buildTotals(result.Pakan, "TOTAL PAKAN")
buildTotals(result.Pullet, "TOTAL PULLET")
return result
}
@@ -538,7 +538,7 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint
var population float64
for _, history := range project.KandangHistory {
for _, chickin := range history.Chickins {
population += chickin.UsageQty + chickin.PendingUsageQty
population += chickin.UsageQty
}
}
@@ -359,7 +359,11 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
if filterFlag == "" {
return true
}
return strings.ToUpper(f) == filterFlag
candidate := strings.ToUpper(f)
if filterFlag == "DOC" || filterFlag == "PULLET" {
return candidate == "DOC" || candidate == "PULLET"
}
return candidate == filterFlag
}
// For project flocks with category GROWING, pullet usage from chickin
@@ -0,0 +1,243 @@
package controller
import (
"math"
"strconv"
"gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/dto"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/services"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/validations"
"gitlab.com/mbugroup/lti-api.git/internal/response"
"github.com/gofiber/fiber/v2"
)
type DailyChecklistController struct {
DailyChecklistService service.DailyChecklistService
}
func NewDailyChecklistController(dailyChecklistService service.DailyChecklistService) *DailyChecklistController {
return &DailyChecklistController{
DailyChecklistService: dailyChecklistService,
}
}
func (u *DailyChecklistController) 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.DailyChecklistService.GetAll(c, query)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.DailyChecklistListDTO]{
Code: fiber.StatusOK,
Status: "success",
Message: "Get all dailyChecklists successfully",
Meta: response.Meta{
Page: query.Page,
Limit: query.Limit,
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
TotalResults: totalResults,
},
Data: dto.ToDailyChecklistListDTOs(result),
})
}
func (u *DailyChecklistController) 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.DailyChecklistService.GetOne(c, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get dailyChecklist successfully",
Data: dto.ToDailyChecklistListDTO(*result),
})
}
func (u *DailyChecklistController) 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.DailyChecklistService.CreateOne(c, req)
if err != nil {
return err
}
return c.Status(fiber.StatusCreated).
JSON(response.Success{
Code: fiber.StatusCreated,
Status: "success",
Message: "Create dailyChecklist successfully",
Data: dto.ToDailyChecklistListDTO(*result),
})
}
func (u *DailyChecklistController) 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.DailyChecklistService.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 dailyChecklist successfully",
Data: dto.ToDailyChecklistListDTO(*result),
})
}
func (u *DailyChecklistController) 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.DailyChecklistService.DeleteOne(c, uint(id)); err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Common{
Code: fiber.StatusOK,
Status: "success",
Message: "Delete dailyChecklist successfully",
})
}
func (u *DailyChecklistController) CreateDailyChecklistPhase(c *fiber.Ctx) error {
param := c.Params("idDailyChecklist")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid daily checklist id")
}
req := new(validation.AssignPhases)
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
if err := u.DailyChecklistService.AssignPhases(c, uint(id), req); err != nil {
return err
}
return c.Status(fiber.StatusCreated).
JSON(response.Success{
Code: fiber.StatusCreated,
Status: "success",
Message: "Daily checklist phases saved successfully",
})
}
func (u *DailyChecklistController) CreateAssignment(c *fiber.Ctx) error {
param := c.Params("idDailyChecklist")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid daily checklist id")
}
req := new(validation.AssignTask)
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
if err := u.DailyChecklistService.AssignTasks(c, uint(id), req); err != nil {
return err
}
return c.Status(fiber.StatusCreated).
JSON(response.Success{
Code: fiber.StatusCreated,
Status: "success",
Message: "Daily checklist assignments saved successfully",
})
}
func (u *DailyChecklistController) RemoveAssignment(c *fiber.Ctx) error {
dailyChecklistParam := c.Params("idDailyChecklist")
employeeParam := c.Params("idEmployee")
dailyChecklistID, err := strconv.Atoi(dailyChecklistParam)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid daily checklist id")
}
employeeID, err := strconv.Atoi(employeeParam)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid employee id")
}
if err := u.DailyChecklistService.RemoveAssignment(c, uint(dailyChecklistID), uint(employeeID)); err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Common{
Code: fiber.StatusOK,
Status: "success",
Message: "Assignment removed successfully",
})
}
func (u *DailyChecklistController) GetAllTasks(c *fiber.Ctx) error {
checklistParam := c.Query("checklist_id", "")
if checklistParam == "" {
return fiber.NewError(fiber.StatusBadRequest, "checklist_id is required")
}
checklistID, err := strconv.Atoi(checklistParam)
if err != nil || checklistID <= 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid checklist_id")
}
result, err := u.DailyChecklistService.GetTasks(c, uint(checklistID))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get daily checklist tasks successfully",
Data: result,
})
}
@@ -0,0 +1,76 @@
package dto
import (
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
)
// === DTO Structs ===
type DailyChecklistRelationDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
}
type DailyChecklistListDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
CreatedUser *userDTO.UserRelationDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type DailyChecklistDetailDTO struct {
DailyChecklistListDTO
}
// === Mapper Functions ===
func ToDailyChecklistRelationDTO(e entity.DailyChecklist) DailyChecklistRelationDTO {
var name string
if e.Name != nil {
name = *e.Name
}
return DailyChecklistRelationDTO{
Id: e.Id,
Name: name,
}
}
func ToDailyChecklistListDTO(e entity.DailyChecklist) DailyChecklistListDTO {
var createdUser *userDTO.UserRelationDTO
// if e.CreatedUser.Id != 0 {
// mapped := userDTO.ToUserRelationDTO(e.CreatedUser)
// createdUser = &mapped
// }
var name string
if e.Name != nil {
name = *e.Name
}
return DailyChecklistListDTO{
Id: e.Id,
Name: name,
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
CreatedUser: createdUser,
}
}
func ToDailyChecklistListDTOs(e []entity.DailyChecklist) []DailyChecklistListDTO {
result := make([]DailyChecklistListDTO, len(e))
for i, r := range e {
result[i] = ToDailyChecklistListDTO(r)
}
return result
}
func ToDailyChecklistDetailDTO(e entity.DailyChecklist) DailyChecklistDetailDTO {
return DailyChecklistDetailDTO{
DailyChecklistListDTO: ToDailyChecklistListDTO(e),
}
}
@@ -0,0 +1,27 @@
package dailyChecklists
import (
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
rDailyChecklist "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/repositories"
sDailyChecklist "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/services"
rPhases "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phasess/repositories"
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
)
type DailyChecklistModule struct{}
func (DailyChecklistModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
dailyChecklistRepo := rDailyChecklist.NewDailyChecklistRepository(db)
phasesRepo := rPhases.NewPhasesRepository(db)
userRepo := rUser.NewUserRepository(db)
dailyChecklistService := sDailyChecklist.NewDailyChecklistService(dailyChecklistRepo, phasesRepo, validate)
userService := sUser.NewUserService(userRepo, validate)
DailyChecklistRoutes(router, userService, dailyChecklistService)
}
@@ -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 DailyChecklistRepository interface {
repository.BaseRepository[entity.DailyChecklist]
}
type DailyChecklistRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.DailyChecklist]
}
func NewDailyChecklistRepository(db *gorm.DB) DailyChecklistRepository {
return &DailyChecklistRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.DailyChecklist](db),
}
}
@@ -0,0 +1,35 @@
package dailyChecklists
import (
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/controllers"
dailyChecklist "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
"github.com/gofiber/fiber/v2"
)
func DailyChecklistRoutes(v1 fiber.Router, u user.UserService, s dailyChecklist.DailyChecklistService) {
ctrl := controller.NewDailyChecklistController(s)
route := v1.Group("/daily-checklists")
route.Use(m.Auth(u))
route.Get("/", ctrl.GetAll)
route.Post("/", ctrl.CreateOne)
// create task
route.Post("/phase/:idDailyChecklist", ctrl.CreateDailyChecklistPhase)
// create assigment
route.Post("/assignment/:idDailyChecklist", ctrl.CreateAssignment)
// remove assignment
route.Delete("/:idDailyChecklist/assignments/:idEmployee", ctrl.RemoveAssignment)
//get all tasks
route.Get("/tasks", ctrl.GetAllTasks)
route.Get("/:id", ctrl.GetOne)
route.Patch("/:id", ctrl.UpdateOne)
route.Delete("/:id", ctrl.DeleteOne)
}
@@ -0,0 +1,410 @@
package service
import (
"errors"
"strconv"
"strings"
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/validations"
phaseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phasess/repositories"
"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"
"gorm.io/gorm/clause"
)
type DailyChecklistService interface {
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.DailyChecklist, int64, error)
GetOne(ctx *fiber.Ctx, id uint) (*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)
DeleteOne(ctx *fiber.Ctx, id uint) error
AssignPhases(ctx *fiber.Ctx, id uint, req *validation.AssignPhases) error
AssignTasks(ctx *fiber.Ctx, id uint, req *validation.AssignTask) error
RemoveAssignment(ctx *fiber.Ctx, id uint, employeeID uint) error
GetTasks(ctx *fiber.Ctx, checklistID uint) ([]entity.DailyChecklistActivityTask, error)
}
type dailyChecklistService struct {
Log *logrus.Logger
Validate *validator.Validate
Repository repository.DailyChecklistRepository
PhaseRepo phaseRepo.PhasesRepository
}
func NewDailyChecklistService(repo repository.DailyChecklistRepository, phaseRepo phaseRepo.PhasesRepository, validate *validator.Validate) DailyChecklistService {
return &dailyChecklistService{
Log: utils.Log,
Validate: validate,
Repository: repo,
PhaseRepo: phaseRepo,
}
}
func (s dailyChecklistService) withRelations(db *gorm.DB) *gorm.DB {
return db
}
func (s dailyChecklistService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.DailyChecklist, int64, error) {
if err := s.Validate.Struct(params); err != nil {
return nil, 0, err
}
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.withRelations(db)
if params.Search != "" {
return db.Where("name LIKE ?", "%"+params.Search+"%")
}
return db.Order("created_at DESC").Order("updated_at DESC")
})
if err != nil {
s.Log.Errorf("Failed to get dailyChecklists: %+v", err)
return nil, 0, err
}
return dailyChecklists, total, nil
}
func (s dailyChecklistService) GetOne(c *fiber.Ctx, id uint) (*entity.DailyChecklist, error) {
dailyChecklist, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "DailyChecklist not found")
}
if err != nil {
s.Log.Errorf("Failed get dailyChecklist by id: %+v", err)
return nil, err
}
return dailyChecklist, nil
}
func (s *dailyChecklistService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.DailyChecklist, 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")
}
status := req.Status
category := req.Category
createBody := &entity.DailyChecklist{
KandangId: req.KandangId,
Date: date,
Category: category,
Status: &status,
}
err = s.Repository.DB().WithContext(c.Context()).Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "date"}, {Name: "kandang_id"}, {Name: "category"}},
DoUpdates: clause.Assignments(map[string]any{"status": status, "updated_at": time.Now()}),
}).Create(createBody).Error
if err != nil {
s.Log.Errorf("Failed to upsert dailyChecklist: %+v", err)
return nil, err
}
return s.GetOne(c, createBody.Id)
}
func (s dailyChecklistService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.DailyChecklist, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
updateBody := make(map[string]any)
if req.Name != nil {
updateBody["name"] = *req.Name
}
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, "DailyChecklist not found")
}
s.Log.Errorf("Failed to update dailyChecklist: %+v", err)
return nil, err
}
return s.GetOne(c, id)
}
func (s dailyChecklistService) 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, "DailyChecklist not found")
}
s.Log.Errorf("Failed to delete dailyChecklist: %+v", err)
return err
}
return nil
}
func (s dailyChecklistService) AssignPhases(c *fiber.Ctx, id uint, req *validation.AssignPhases) error {
if err := s.Validate.Struct(req); err != nil {
return err
}
if _, err := s.Repository.GetByID(c.Context(), id, nil); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, "DailyChecklist not found")
}
return err
}
phaseIDs, err := parsePhaseIDs(req.PhaseIDs)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
if len(phaseIDs) > 0 {
phases, err := s.PhaseRepo.GetByIDs(c.Context(), phaseIDs, nil)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusBadRequest, "Phase not found")
}
return err
}
if len(phases) != len(phaseIDs) {
return fiber.NewError(fiber.StatusBadRequest, "Phase not found")
}
}
db := s.Repository.DB()
if err := db.WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
if err := tx.Where("checklist_id = ?", id).Delete(&entity.DailyChecklistPhase{}).Error; err != nil {
return err
}
if len(phaseIDs) == 0 {
return nil
}
records := make([]entity.DailyChecklistPhase, 0, len(phaseIDs))
for _, pid := range phaseIDs {
records = append(records, entity.DailyChecklistPhase{
ChecklistId: id,
PhaseId: pid,
})
}
if err := tx.Create(&records).Error; err != nil {
return err
}
if err := tx.Where("checklist_id = ?", id).Delete(&entity.DailyChecklistActivityTask{}).Error; err != nil {
return err
}
var activities []entity.PhaseActivity
if err := tx.Where("phase_id IN ?", phaseIDs).Find(&activities).Error; err != nil {
return err
}
activityRecords := make([]entity.DailyChecklistActivityTask, 0, len(activities))
for _, activity := range activities {
activityRecords = append(activityRecords, entity.DailyChecklistActivityTask{
ChecklistId: id,
PhaseId: activity.PhaseId,
PhaseActivityId: activity.Id,
TimeType: activity.TimeType,
})
}
if len(activityRecords) == 0 {
return nil
}
return tx.Create(&activityRecords).Error
}); err != nil {
s.Log.Errorf("Failed to assign phases to daily checklist: %+v", err)
return err
}
return nil
}
func (s dailyChecklistService) RemoveAssignment(c *fiber.Ctx, id uint, employeeID uint) error {
if _, err := s.Repository.GetByID(c.Context(), id, nil); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, "DailyChecklist not found")
}
return err
}
if employeeID == 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid employee id")
}
db := s.Repository.DB()
if err := db.WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
var tasks []entity.DailyChecklistActivityTask
if err := tx.Where("checklist_id = ?", id).Find(&tasks).Error; err != nil {
return err
}
if len(tasks) == 0 {
return fiber.NewError(fiber.StatusBadRequest, "No activity tasks found for this checklist")
}
taskIDs := collectTaskIDs(tasks)
return tx.Where("task_id IN ? AND employee_id = ?", taskIDs, employeeID).
Delete(&entity.DailyChecklistActivityTaskAssignment{}).Error
}); err != nil {
s.Log.Errorf("Failed to remove assignment: %+v", err)
return err
}
return nil
}
func (s dailyChecklistService) GetTasks(c *fiber.Ctx, checklistID uint) ([]entity.DailyChecklistActivityTask, 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 tasks []entity.DailyChecklistActivityTask
if err := s.Repository.DB().WithContext(c.Context()).
Where("checklist_id = ?", checklistID).
Order("created_at ASC").
Find(&tasks).Error; err != nil {
s.Log.Errorf("Failed to get daily checklist tasks: %+v", err)
return nil, err
}
return tasks, nil
}
func parsePhaseIDs(raw string) ([]uint, error) {
parts := strings.Split(raw, ",")
result := make([]uint, 0, len(parts))
seen := make(map[uint]struct{})
for _, part := range parts {
value := strings.TrimSpace(part)
if value == "" {
continue
}
num, err := strconv.ParseUint(value, 10, 64)
if err != nil {
return nil, errors.New("invalid phase id: " + value)
}
u := uint(num)
if _, ok := seen[u]; ok {
continue
}
seen[u] = struct{}{}
result = append(result, u)
}
return result, nil
}
func parseIDs(raw string) ([]uint, error) {
parts := strings.Split(raw, ",")
result := make([]uint, 0, len(parts))
seen := make(map[uint]struct{})
for _, part := range parts {
value := strings.TrimSpace(part)
if value == "" {
continue
}
num, err := strconv.ParseUint(value, 10, 64)
if err != nil {
return nil, errors.New("invalid employee id: " + value)
}
u := uint(num)
if _, ok := seen[u]; ok {
continue
}
seen[u] = struct{}{}
result = append(result, u)
}
return result, nil
}
func collectTaskIDs(tasks []entity.DailyChecklistActivityTask) []uint {
result := make([]uint, len(tasks))
for i, task := range tasks {
result[i] = task.Id
}
return result
}
func (s dailyChecklistService) AssignTasks(c *fiber.Ctx, id uint, req *validation.AssignTask) error {
if err := s.Validate.Struct(req); err != nil {
return err
}
employeeIDs, err := parseIDs(req.EmployeeIDs)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
if len(employeeIDs) == 0 {
return fiber.NewError(fiber.StatusBadRequest, "employee_ids cannot be empty")
}
if _, err := s.Repository.GetByID(c.Context(), id, nil); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, "DailyChecklist not found")
}
return err
}
db := s.Repository.DB()
if err := db.WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
var tasks []entity.DailyChecklistActivityTask
if err := tx.Where("checklist_id = ?", id).Find(&tasks).Error; err != nil {
return err
}
if len(tasks) == 0 {
return fiber.NewError(fiber.StatusBadRequest, "No activity tasks found for this checklist")
}
assignments := make([]entity.DailyChecklistActivityTaskAssignment, 0, len(tasks)*len(employeeIDs))
for _, task := range tasks {
for _, empID := range employeeIDs {
assignments = append(assignments, entity.DailyChecklistActivityTaskAssignment{
TaskId: task.Id,
EmployeeId: empID,
})
}
}
return tx.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "task_id"}, {Name: "employee_id"}},
DoUpdates: clause.Assignments(map[string]any{"updated_at": time.Now()}),
}).Create(&assignments).Error
}); err != nil {
s.Log.Errorf("Failed to assign tasks to daily checklist: %+v", err)
return err
}
return nil
}
@@ -0,0 +1,26 @@
package validation
type Create struct {
Date string `json:"date" validate:"required"`
KandangId uint `json:"kandang_id" validate:"required"`
Category string `json:"category" validate:"required"`
Status string `json:"status" validate:"required"`
}
type Update struct {
Name *string `json:"name,omitempty" validate:"omitempty"`
}
type Query struct {
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
Search string `query:"search" validate:"omitempty,max=50"`
}
type AssignPhases struct {
PhaseIDs string `json:"phase_ids" validate:"required"`
}
type AssignTask struct {
EmployeeIDs string `json:"employee_ids" validate:"required"`
}
@@ -0,0 +1,161 @@
package controller
import (
"math"
"strconv"
"gitlab.com/mbugroup/lti-api.git/internal/modules/master/employees/dto"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/master/employees/services"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/employees/validations"
"gitlab.com/mbugroup/lti-api.git/internal/response"
"github.com/gofiber/fiber/v2"
)
type EmployeesController struct {
EmployeesService service.EmployeesService
}
func NewEmployeesController(employeesService service.EmployeesService) *EmployeesController {
return &EmployeesController{
EmployeesService: employeesService,
}
}
func (u *EmployeesController) 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")
}
if kandangParam := c.Query("kandang_id", ""); kandangParam != "" {
id, err := strconv.Atoi(kandangParam)
if err != nil || id <= 0 {
return fiber.NewError(fiber.StatusBadRequest, "invalid kandang_id")
}
temp := uint(id)
query.KandangId = &temp
}
if activeParam := c.Query("is_active", ""); activeParam != "" {
value, err := strconv.ParseBool(activeParam)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "invalid is_active value")
}
query.IsActive = &value
}
result, totalResults, err := u.EmployeesService.GetAll(c, query)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.EmployeesListDTO]{
Code: fiber.StatusOK,
Status: "success",
Message: "Get all employeess successfully",
Meta: response.Meta{
Page: query.Page,
Limit: query.Limit,
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
TotalResults: totalResults,
},
Data: dto.ToEmployeesListDTOs(result),
})
}
func (u *EmployeesController) 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.EmployeesService.GetOne(c, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get employees successfully",
Data: dto.ToEmployeesListDTO(*result),
})
}
func (u *EmployeesController) 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.EmployeesService.CreateOne(c, req)
if err != nil {
return err
}
return c.Status(fiber.StatusCreated).
JSON(response.Success{
Code: fiber.StatusCreated,
Status: "success",
Message: "Create employees successfully",
Data: dto.ToEmployeesListDTO(*result),
})
}
func (u *EmployeesController) 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.EmployeesService.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 employees successfully",
Data: dto.ToEmployeesListDTO(*result),
})
}
func (u *EmployeesController) 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.EmployeesService.DeleteOne(c, uint(id)); err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Common{
Code: fiber.StatusOK,
Status: "success",
Message: "Delete employees successfully",
})
}
@@ -0,0 +1,70 @@
package dto
import (
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
kandangDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto"
)
// === DTO Structs ===
type EmployeesRelationDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
}
type EmployeesListDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
IsActive bool `json:"is_active"`
Kandangs []kandangDTO.KandangRelationDTO `json:"kandangs"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type EmployeesDetailDTO struct {
EmployeesListDTO
}
// === Mapper Functions ===
func ToEmployeesRelationDTO(e entity.Employees) EmployeesRelationDTO {
return EmployeesRelationDTO{
Id: e.Id,
Name: e.Name,
}
}
func ToEmployeesListDTO(e entity.Employees) EmployeesListDTO {
kandangs := make([]kandangDTO.KandangRelationDTO, 0, len(e.EmployeeKandangs))
for _, rel := range e.EmployeeKandangs {
if rel.Kandang.Id == 0 {
continue
}
kandangs = append(kandangs, kandangDTO.ToKandangRelationDTO(rel.Kandang))
}
return EmployeesListDTO{
Id: e.Id,
Name: e.Name,
IsActive: e.IsActive,
Kandangs: kandangs,
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
}
}
func ToEmployeesListDTOs(e []entity.Employees) []EmployeesListDTO {
result := make([]EmployeesListDTO, len(e))
for i, r := range e {
result[i] = ToEmployeesListDTO(r)
}
return result
}
func ToEmployeesDetailDTO(e entity.Employees) EmployeesDetailDTO {
return EmployeesDetailDTO{
EmployeesListDTO: ToEmployeesListDTO(e),
}
}
@@ -0,0 +1,25 @@
package employeess
import (
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
rEmployees "gitlab.com/mbugroup/lti-api.git/internal/modules/master/employees/repositories"
sEmployees "gitlab.com/mbugroup/lti-api.git/internal/modules/master/employees/services"
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
)
type EmployeesModule struct{}
func (EmployeesModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
employeesRepo := rEmployees.NewEmployeesRepository(db)
userRepo := rUser.NewUserRepository(db)
employeesService := sEmployees.NewEmployeesService(employeesRepo, validate)
userService := sUser.NewUserService(userRepo, validate)
EmployeesRoutes(router, userService, employeesService)
}
@@ -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 EmployeesRepository interface {
repository.BaseRepository[entity.Employees]
}
type EmployeesRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.Employees]
}
func NewEmployeesRepository(db *gorm.DB) EmployeesRepository {
return &EmployeesRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.Employees](db),
}
}
@@ -0,0 +1,23 @@
package employeess
import (
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/employees/controllers"
employees "gitlab.com/mbugroup/lti-api.git/internal/modules/master/employees/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
"github.com/gofiber/fiber/v2"
)
func EmployeesRoutes(v1 fiber.Router, u user.UserService, s employees.EmployeesService) {
ctrl := controller.NewEmployeesController(s)
route := v1.Group("/employees")
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,265 @@
package service
import (
"errors"
"strings"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/employees/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/employees/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 EmployeesService interface {
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.Employees, int64, error)
GetOne(ctx *fiber.Ctx, id uint) (*entity.Employees, error)
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.Employees, error)
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.Employees, error)
DeleteOne(ctx *fiber.Ctx, id uint) error
}
type employeesService struct {
Log *logrus.Logger
Validate *validator.Validate
Repository repository.EmployeesRepository
}
func NewEmployeesService(repo repository.EmployeesRepository, validate *validator.Validate) EmployeesService {
return &employeesService{
Log: utils.Log,
Validate: validate,
Repository: repo,
}
}
func (s employeesService) withRelations(db *gorm.DB) *gorm.DB {
return db.
Preload("EmployeeKandangs.Kandang").
Where("employees.deleted_at IS NULL")
}
func (s employeesService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Employees, int64, error) {
if err := s.Validate.Struct(params); err != nil {
return nil, 0, err
}
offset := (params.Page - 1) * params.Limit
employeess, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db)
if params.Search != "" {
db = db.Where("employees.name LIKE ?", "%"+params.Search+"%")
}
if params.KandangId != nil {
db = db.Joins("JOIN employee_kandangs ek ON ek.employee_id = employees.id").
Where("ek.kandang_id = ?", *params.KandangId)
}
if params.IsActive != nil {
db = db.Where("employees.is_active = ?", *params.IsActive)
}
return db.Order("employees.created_at DESC").Order("employees.updated_at DESC")
})
if err != nil {
s.Log.Errorf("Failed to get employeess: %+v", err)
return nil, 0, err
}
return employeess, total, nil
}
func (s employeesService) GetOne(c *fiber.Ctx, id uint) (*entity.Employees, error) {
employees, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Employees not found")
}
if err != nil {
s.Log.Errorf("Failed get employees by id: %+v", err)
return nil, err
}
return employees, nil
}
func (s *employeesService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.Employees, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
name := strings.TrimSpace(req.Name)
if name == "" {
return nil, fiber.NewError(fiber.StatusBadRequest, "name cannot be empty")
}
kandangIDs := normalizeKandangIDs(req.KandangIDs)
if len(kandangIDs) == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "kandang_ids must contain at least one valid id")
}
if _, err := s.Repository.First(c.Context(), func(db *gorm.DB) *gorm.DB {
return db.Where("LOWER(name) = ?", strings.ToLower(name))
}); err == nil {
return nil, fiber.NewError(fiber.StatusBadRequest, "employee already exists")
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
s.Log.Errorf("Failed checking employee uniqueness: %+v", err)
return nil, err
}
createBody := &entity.Employees{
Name: name,
IsActive: req.IsActive,
}
if err := s.Repository.DB().Transaction(func(tx *gorm.DB) error {
repoTx := s.Repository.WithTx(tx)
if err := repoTx.CreateOne(c.Context(), createBody, nil); err != nil {
return err
}
relations := make([]entity.EmployeeKandang, 0, len(kandangIDs))
for _, kandangID := range kandangIDs {
relations = append(relations, entity.EmployeeKandang{
EmployeeId: createBody.Id,
KandangId: kandangID,
})
}
if len(relations) > 0 {
if err := tx.WithContext(c.Context()).Create(&relations).Error; err != nil {
return err
}
}
return nil
}); err != nil {
s.Log.Errorf("Failed to create employees: %+v", err)
return nil, err
}
return s.GetOne(c, createBody.Id)
}
func (s employeesService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.Employees, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
updateBody := make(map[string]any)
var (
kandangIDs []uint
needKandangUpdate bool
)
if req.Name != nil {
trimmed := strings.TrimSpace(*req.Name)
if trimmed == "" {
return nil, fiber.NewError(fiber.StatusBadRequest, "name cannot be empty")
}
if _, err := s.Repository.First(c.Context(), func(db *gorm.DB) *gorm.DB {
return db.Where("LOWER(name) = ? AND id <> ?", strings.ToLower(trimmed), id)
}); err == nil {
return nil, fiber.NewError(fiber.StatusBadRequest, "employee already exists")
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
s.Log.Errorf("Failed checking employee uniqueness: %+v", err)
return nil, err
}
updateBody["name"] = trimmed
}
if req.IsActive != nil {
updateBody["is_active"] = *req.IsActive
}
if req.KandangIDs != nil {
ids := normalizeKandangIDs(*req.KandangIDs)
if len(ids) == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "kandang_ids must contain at least one valid id")
}
kandangIDs = ids
needKandangUpdate = true
}
if len(updateBody) == 0 && !needKandangUpdate {
return s.GetOne(c, id)
}
if err := s.Repository.DB().Transaction(func(tx *gorm.DB) error {
repoTx := s.Repository.WithTx(tx)
if len(updateBody) > 0 {
if err := repoTx.PatchOne(c.Context(), id, updateBody, nil); err != nil {
return err
}
}
if needKandangUpdate {
if err := tx.WithContext(c.Context()).
Where("employee_id = ?", id).
Delete(&entity.EmployeeKandang{}).Error; err != nil {
return err
}
relations := make([]entity.EmployeeKandang, 0, len(kandangIDs))
for _, kandangID := range kandangIDs {
relations = append(relations, entity.EmployeeKandang{
EmployeeId: id,
KandangId: kandangID,
})
}
if len(relations) > 0 {
if err := tx.WithContext(c.Context()).Create(&relations).Error; err != nil {
return err
}
}
}
return nil
}); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Employees not found")
}
s.Log.Errorf("Failed to update employees: %+v", err)
return nil, err
}
return s.GetOne(c, id)
}
func (s employeesService) 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, "Employees not found")
}
s.Log.Errorf("Failed to delete employees: %+v", err)
return err
}
return nil
}
func normalizeKandangIDs(ids []uint) []uint {
result := make([]uint, 0, len(ids))
seen := make(map[uint]struct{})
for _, id := range ids {
if id == 0 {
continue
}
if _, ok := seen[id]; ok {
continue
}
seen[id] = struct{}{}
result = append(result, id)
}
return result
}
@@ -0,0 +1,21 @@
package validation
type Create struct {
Name string `json:"name" validate:"required_strict,min=3"`
KandangIDs []uint `json:"kandang_ids" validate:"required,min=1,dive,required"`
IsActive bool `json:"is_active"`
}
type Update struct {
Name *string `json:"name,omitempty" validate:"omitempty"`
KandangIDs *[]uint `json:"kandang_ids,omitempty" validate:"omitempty,min=1,dive,required"`
IsActive *bool `json:"is_active,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"`
KandangId *uint `query:"kandang_id" validate:"omitempty"`
IsActive *bool `query:"is_active" validate:"omitempty"`
}
@@ -0,0 +1,153 @@
package controller
import (
"math"
"strconv"
"gitlab.com/mbugroup/lti-api.git/internal/modules/master/phase-activities/dto"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phase-activities/services"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phase-activities/validations"
"gitlab.com/mbugroup/lti-api.git/internal/response"
"github.com/gofiber/fiber/v2"
)
type PhaseActivityController struct {
PhaseActivityService service.PhaseActivityService
}
func NewPhaseActivityController(phaseActivityService service.PhaseActivityService) *PhaseActivityController {
return &PhaseActivityController{
PhaseActivityService: phaseActivityService,
}
}
func (u *PhaseActivityController) 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")
}
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)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.PhaseActivityListDTO]{
Code: fiber.StatusOK,
Status: "success",
Message: "Get all phaseActivitys successfully",
Meta: response.Meta{
Page: query.Page,
Limit: query.Limit,
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
TotalResults: totalResults,
},
Data: dto.ToPhaseActivityListDTOs(result),
})
}
func (u *PhaseActivityController) 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.PhaseActivityService.GetOne(c, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get phaseActivity successfully",
Data: dto.ToPhaseActivityListDTO(*result),
})
}
func (u *PhaseActivityController) 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.PhaseActivityService.CreateOne(c, req)
if err != nil {
return err
}
return c.Status(fiber.StatusCreated).
JSON(response.Success{
Code: fiber.StatusCreated,
Status: "success",
Message: "Create phaseActivity successfully",
Data: dto.ToPhaseActivityListDTO(*result),
})
}
func (u *PhaseActivityController) 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.PhaseActivityService.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 phaseActivity successfully",
Data: dto.ToPhaseActivityListDTO(*result),
})
}
func (u *PhaseActivityController) 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.PhaseActivityService.DeleteOne(c, uint(id)); err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Common{
Code: fiber.StatusOK,
Status: "success",
Message: "Delete phaseActivity successfully",
})
}
@@ -0,0 +1,72 @@
package dto
import (
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
)
// === DTO Structs ===
type PhaseActivityRelationDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
}
type PhaseActivityListDTO struct {
Id uint `json:"id"`
PhaseId uint `json:"phase_id"`
Name string `json:"name"`
Description *string `json:"description"`
TimeType *string `json:"time_type"`
CreatedUser *userDTO.UserRelationDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type PhaseActivityDetailDTO struct {
PhaseActivityListDTO
}
// === Mapper Functions ===
func ToPhaseActivityRelationDTO(e entity.PhaseActivity) PhaseActivityRelationDTO {
return PhaseActivityRelationDTO{
Id: e.Id,
Name: e.Name,
}
}
func ToPhaseActivityListDTO(e entity.PhaseActivity) PhaseActivityListDTO {
var createdUser *userDTO.UserRelationDTO
// if e.CreatedUser.Id != 0 {
// mapped := userDTO.ToUserRelationDTO(e.CreatedUser)
// createdUser = &mapped
// }
return PhaseActivityListDTO{
Id: e.Id,
PhaseId: e.PhaseId,
Name: e.Name,
Description: e.Description,
TimeType: e.TimeType,
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
CreatedUser: createdUser,
}
}
func ToPhaseActivityListDTOs(e []entity.PhaseActivity) []PhaseActivityListDTO {
result := make([]PhaseActivityListDTO, len(e))
for i, r := range e {
result[i] = ToPhaseActivityListDTO(r)
}
return result
}
func ToPhaseActivityDetailDTO(e entity.PhaseActivity) PhaseActivityDetailDTO {
return PhaseActivityDetailDTO{
PhaseActivityListDTO: ToPhaseActivityListDTO(e),
}
}
@@ -0,0 +1,27 @@
package phaseActivity
import (
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
rPhaseActivity "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phase-activities/repositories"
sPhaseActivity "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phase-activities/services"
rPhases "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phasess/repositories"
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
)
type PhaseActivityModule struct{}
func (PhaseActivityModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
phaseActivityRepo := rPhaseActivity.NewPhaseActivityRepository(db)
phasesRepo := rPhases.NewPhasesRepository(db)
userRepo := rUser.NewUserRepository(db)
phaseActivityService := sPhaseActivity.NewPhaseActivityService(phaseActivityRepo, phasesRepo, validate)
userService := sUser.NewUserService(userRepo, validate)
PhaseActivityRoutes(router, userService, phaseActivityService)
}
@@ -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 PhaseActivityRepository interface {
repository.BaseRepository[entity.PhaseActivity]
}
type PhaseActivityRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.PhaseActivity]
}
func NewPhaseActivityRepository(db *gorm.DB) PhaseActivityRepository {
return &PhaseActivityRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.PhaseActivity](db),
}
}
@@ -0,0 +1,23 @@
package phaseActivity
import (
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phase-activities/controllers"
phaseActivity "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phase-activities/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
"github.com/gofiber/fiber/v2"
)
func PhaseActivityRoutes(v1 fiber.Router, u user.UserService, s phaseActivity.PhaseActivityService) {
ctrl := controller.NewPhaseActivityController(s)
route := v1.Group("/phase-activities")
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,167 @@
package service
import (
"errors"
"strings"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phase-activities/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phase-activities/validations"
phaseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phasess/repositories"
"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 PhaseActivityService interface {
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.PhaseActivity, int64, error)
GetOne(ctx *fiber.Ctx, id uint) (*entity.PhaseActivity, error)
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.PhaseActivity, error)
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.PhaseActivity, error)
DeleteOne(ctx *fiber.Ctx, id uint) error
}
type phaseActivityService struct {
Log *logrus.Logger
Validate *validator.Validate
Repository repository.PhaseActivityRepository
PhaseRepo phaseRepo.PhasesRepository
}
func NewPhaseActivityService(repo repository.PhaseActivityRepository, phaseRepo phaseRepo.PhasesRepository, validate *validator.Validate) PhaseActivityService {
return &phaseActivityService{
Log: utils.Log,
Validate: validate,
Repository: repo,
PhaseRepo: phaseRepo,
}
}
func (s phaseActivityService) withRelations(db *gorm.DB) *gorm.DB {
return db
}
func (s phaseActivityService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.PhaseActivity, int64, error) {
if err := s.Validate.Struct(params); err != nil {
return nil, 0, err
}
offset := (params.Page - 1) * params.Limit
phaseActivitys, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db)
if params.Search != "" {
db = db.Where("name LIKE ?", "%"+params.Search+"%")
}
if params.PhaseId != nil {
db = db.Where("phase_id = ?", *params.PhaseId)
}
return db.Order("created_at DESC").Order("updated_at DESC")
})
if err != nil {
s.Log.Errorf("Failed to get phaseActivitys: %+v", err)
return nil, 0, err
}
return phaseActivitys, total, nil
}
func (s phaseActivityService) GetOne(c *fiber.Ctx, id uint) (*entity.PhaseActivity, error) {
phaseActivity, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "PhaseActivity not found")
}
if err != nil {
s.Log.Errorf("Failed get phaseActivity by id: %+v", err)
return nil, err
}
return phaseActivity, nil
}
func (s *phaseActivityService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.PhaseActivity, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
phase, err := s.PhaseRepo.GetByID(c.Context(), req.PhaseId, nil)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusBadRequest, "phase not found")
}
if err != nil {
s.Log.Errorf("Failed to get phase: %+v", err)
return nil, err
}
name := strings.TrimSpace(req.Name)
if name == "" {
return nil, fiber.NewError(fiber.StatusBadRequest, "name cannot be empty")
}
timeType := strings.TrimSpace(req.TimeType)
if timeType == "" {
return nil, fiber.NewError(fiber.StatusBadRequest, "time_type cannot be empty")
}
createBody := &entity.PhaseActivity{
PhaseId: phase.Id,
Name: name,
Description: req.Description,
TimeType: &timeType,
}
if err := s.Repository.CreateOne(c.Context(), createBody, nil); err != nil {
s.Log.Errorf("Failed to create phaseActivity: %+v", err)
return nil, err
}
return s.GetOne(c, createBody.Id)
}
func (s phaseActivityService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.PhaseActivity, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
trimmedName := strings.TrimSpace(req.Name)
if trimmedName == "" {
return nil, fiber.NewError(fiber.StatusBadRequest, "name cannot be empty")
}
trimmedTimeType := strings.TrimSpace(req.TimeType)
if trimmedTimeType == "" {
return nil, fiber.NewError(fiber.StatusBadRequest, "time_type cannot be empty")
}
updateBody := map[string]any{
"name": trimmedName,
"time_type": trimmedTimeType,
}
if req.Description != nil {
updateBody["description"] = *req.Description
}
if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "PhaseActivity not found")
}
s.Log.Errorf("Failed to update phaseActivity: %+v", err)
return nil, err
}
return s.GetOne(c, id)
}
func (s phaseActivityService) 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, "PhaseActivity not found")
}
s.Log.Errorf("Failed to delete phaseActivity: %+v", err)
return err
}
return nil
}
@@ -0,0 +1,21 @@
package validation
type Create struct {
PhaseId uint `json:"phase_id" validate:"required"`
Name string `json:"name" validate:"required_strict,min=3"`
Description *string `json:"description,omitempty"`
TimeType string `json:"time_type" validate:"required"`
}
type Update struct {
Name string `json:"name" validate:"required_strict,min=3"`
Description *string `json:"description,omitempty"`
TimeType string `json:"time_type" validate:"required"`
}
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"`
PhaseId *uint `query:"phase_id" validate:"omitempty"`
}
@@ -0,0 +1,148 @@
package controller
import (
"math"
"strconv"
"gitlab.com/mbugroup/lti-api.git/internal/modules/master/phasess/dto"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phasess/services"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phasess/validations"
"gitlab.com/mbugroup/lti-api.git/internal/response"
"github.com/gofiber/fiber/v2"
)
type PhasesController struct {
PhasesService service.PhasesService
}
func NewPhasesController(phasesService service.PhasesService) *PhasesController {
return &PhasesController{
PhasesService: phasesService,
}
}
func (u *PhasesController) 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")
}
if category := c.Query("category", ""); category != "" {
query.Category = &category
}
result, totalResults, err := u.PhasesService.GetAll(c, query)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.PhasesListDTO]{
Code: fiber.StatusOK,
Status: "success",
Message: "Get all phasess successfully",
Meta: response.Meta{
Page: query.Page,
Limit: query.Limit,
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
TotalResults: totalResults,
},
Data: dto.ToPhasesListDTOs(result),
})
}
func (u *PhasesController) 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.PhasesService.GetOne(c, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get phases successfully",
Data: dto.ToPhasesListDTO(*result),
})
}
func (u *PhasesController) 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.PhasesService.CreateOne(c, req)
if err != nil {
return err
}
return c.Status(fiber.StatusCreated).
JSON(response.Success{
Code: fiber.StatusCreated,
Status: "success",
Message: "Create phases successfully",
Data: dto.ToPhasesListDTO(*result),
})
}
func (u *PhasesController) 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.PhasesService.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 phases successfully",
Data: dto.ToPhasesListDTO(*result),
})
}
func (u *PhasesController) 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.PhasesService.DeleteOne(c, uint(id)); err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Common{
Code: fiber.StatusOK,
Status: "success",
Message: "Delete phases successfully",
})
}
@@ -0,0 +1,68 @@
package dto
import (
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
)
// === DTO Structs ===
type PhasesRelationDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
}
type PhasesListDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
Category string `json:"category"`
IsActive bool `json:"is_active"`
CreatedUser *userDTO.UserRelationDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"`
}
type PhasesDetailDTO struct {
PhasesListDTO
}
// === Mapper Functions ===
func ToPhasesRelationDTO(e entity.Phases) PhasesRelationDTO {
return PhasesRelationDTO{
Id: e.Id,
Name: e.Name,
}
}
func ToPhasesListDTO(e entity.Phases) PhasesListDTO {
var createdUser *userDTO.UserRelationDTO
// if e.CreatedUser.Id != 0 {
// mapped := userDTO.ToUserRelationDTO(e.CreatedUser)
// createdUser = &mapped
// }
return PhasesListDTO{
Id: e.Id,
Name: e.Name,
Category: e.Category,
IsActive: e.IsActive,
CreatedAt: e.CreatedAt,
CreatedUser: createdUser,
}
}
func ToPhasesListDTOs(e []entity.Phases) []PhasesListDTO {
result := make([]PhasesListDTO, len(e))
for i, r := range e {
result[i] = ToPhasesListDTO(r)
}
return result
}
func ToPhasesDetailDTO(e entity.Phases) PhasesDetailDTO {
return PhasesDetailDTO{
PhasesListDTO: ToPhasesListDTO(e),
}
}
+25
View File
@@ -0,0 +1,25 @@
package phases
import (
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
rPhases "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phasess/repositories"
sPhases "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phasess/services"
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
)
type PhasesModule struct{}
func (PhasesModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
phasesRepo := rPhases.NewPhasesRepository(db)
userRepo := rUser.NewUserRepository(db)
phasesService := sPhases.NewPhasesService(phasesRepo, validate)
userService := sUser.NewUserService(userRepo, validate)
PhasesRoutes(router, userService, phasesService)
}
@@ -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 PhasesRepository interface {
repository.BaseRepository[entity.Phases]
}
type PhasesRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.Phases]
}
func NewPhasesRepository(db *gorm.DB) PhasesRepository {
return &PhasesRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.Phases](db),
}
}
+23
View File
@@ -0,0 +1,23 @@
package phases
import (
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phasess/controllers"
phases "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phasess/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
"github.com/gofiber/fiber/v2"
)
func PhasesRoutes(v1 fiber.Router, u user.UserService, s phases.PhasesService) {
ctrl := controller.NewPhasesController(s)
route := v1.Group("/phases")
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,158 @@
package service
import (
"errors"
"strings"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phasess/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phasess/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 PhasesService interface {
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.Phases, int64, error)
GetOne(ctx *fiber.Ctx, id uint) (*entity.Phases, error)
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.Phases, error)
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.Phases, error)
DeleteOne(ctx *fiber.Ctx, id uint) error
}
type phasesService struct {
Log *logrus.Logger
Validate *validator.Validate
Repository repository.PhasesRepository
}
func NewPhasesService(repo repository.PhasesRepository, validate *validator.Validate) PhasesService {
return &phasesService{
Log: utils.Log,
Validate: validate,
Repository: repo,
}
}
func (s phasesService) withRelations(db *gorm.DB) *gorm.DB {
return db
}
func (s phasesService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Phases, int64, error) {
if err := s.Validate.Struct(params); err != nil {
return nil, 0, err
}
offset := (params.Page - 1) * params.Limit
phasess, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db)
if params.Search != "" {
return db.Where("name LIKE ?", "%"+params.Search+"%")
}
if params.Category != nil {
db = db.Where("category = ?", *params.Category)
}
return db.Order("created_at DESC")
})
if err != nil {
s.Log.Errorf("Failed to get phasess: %+v", err)
return nil, 0, err
}
return phasess, total, nil
}
func (s phasesService) GetOne(c *fiber.Ctx, id uint) (*entity.Phases, error) {
phases, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Phases not found")
}
if err != nil {
s.Log.Errorf("Failed get phases by id: %+v", err)
return nil, err
}
return phases, nil
}
func (s *phasesService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.Phases, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
if _, err := s.Repository.First(c.Context(), func(db *gorm.DB) *gorm.DB {
return db.Where("LOWER(name) = ? AND category = ?", strings.ToLower(req.Name), req.Category)
}); err == nil {
return nil, fiber.NewError(fiber.StatusBadRequest, "phase already exists")
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
s.Log.Errorf("Failed checking phase uniqueness: %+v", err)
return nil, err
}
createBody := &entity.Phases{
Name: req.Name,
Category: req.Category,
IsActive: true,
}
if err := s.Repository.CreateOne(c.Context(), createBody, nil); err != nil {
s.Log.Errorf("Failed to create phases: %+v", err)
return nil, err
}
return s.GetOne(c, createBody.Id)
}
func (s phasesService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.Phases, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
existing, err := s.Repository.GetByID(c.Context(), id, nil)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Phases not found")
}
if err != nil {
s.Log.Errorf("Failed get phases by id: %+v", err)
return nil, err
}
updateBody := make(map[string]any)
if req.Name != nil {
if _, err := s.Repository.First(c.Context(), func(db *gorm.DB) *gorm.DB {
return db.Where("LOWER(name) = ? AND category = ? AND id <> ?", strings.ToLower(*req.Name), existing.Category, id)
}); err == nil {
return nil, fiber.NewError(fiber.StatusBadRequest, "phase already exists")
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
s.Log.Errorf("Failed checking phase uniqueness: %+v", err)
return nil, err
}
updateBody["name"] = strings.TrimSpace(*req.Name)
}
if len(updateBody) == 0 {
return s.GetOne(c, id)
}
if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil {
s.Log.Errorf("Failed to update phases: %+v", err)
return nil, err
}
return s.GetOne(c, id)
}
func (s phasesService) 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, "Phases not found")
}
s.Log.Errorf("Failed to delete phases: %+v", err)
return err
}
return nil
}
@@ -0,0 +1,17 @@
package validation
type Create struct {
Name string `json:"name" validate:"required_strict,min=3"`
Category string `json:"category" validate:"required"`
}
type Update struct {
Name *string `json:"name,omitempty" validate:"omitempty"`
}
type Query struct {
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
Search string `query:"search" validate:"omitempty,max=50"`
Category *string `query:"category" validate:"omitempty"`
}
+7 -1
View File
@@ -10,17 +10,20 @@ import (
areas "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas"
banks "gitlab.com/mbugroup/lti-api.git/internal/modules/master/banks"
customers "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers"
employeess "gitlab.com/mbugroup/lti-api.git/internal/modules/master/employees"
fcrs "gitlab.com/mbugroup/lti-api.git/internal/modules/master/fcrs"
flocks "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks"
kandangs "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs"
locations "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations"
nonstocks "gitlab.com/mbugroup/lti-api.git/internal/modules/master/nonstocks"
phaseActivitys "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phase-activities"
phasess "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phasess"
productcategories "gitlab.com/mbugroup/lti-api.git/internal/modules/master/product-categories"
productionStandards "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards"
products "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products"
suppliers "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers"
uoms "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms"
warehouses "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses"
productionStandards "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards"
// MODULE IMPORTS
)
@@ -42,6 +45,9 @@ func RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Valida
banks.BankModule{},
flocks.FlockModule{},
productionStandards.ProductionStandardModule{},
employeess.EmployeesModule{},
phasess.PhasesModule{},
phaseActivitys.PhaseActivityModule{},
// MODULE REGISTRY
}
@@ -517,27 +517,6 @@ func (s projectflockService) GetAvailableDocQuantity(ctx *fiber.Ctx, kandangID u
return total, nil
}
// getProjectFlockClosingDate mengembalikan tanggal closing Project Flock jika sudah mencapai step SELESAI (Approved).
// func (s projectflockService) getProjectFlockClosingDate(ctx context.Context, projectFlockID uint) (*time.Time, error) {
// if projectFlockID == 0 || s.ApprovalSvc == nil {
// return nil, nil
// }
// latest, err := s.ApprovalSvc.LatestByTarget(ctx, utils.ApprovalWorkflowProjectFlock, projectFlockID, nil)
// if err != nil {
// return nil, err
// }
// if latest == nil || latest.Action == nil || *latest.Action != entity.ApprovalActionApproved {
// return nil, nil
// }
// if latest.StepNumber != uint16(utils.ProjectFlockStepSelesai) {
// return nil, nil
// }
// t := latest.ActionAt
// return &t, nil
// }
func (s projectflockService) GetProjectPeriods(c *fiber.Ctx, projectIDs []uint) (map[uint]int, error) {
if len(projectIDs) == 0 {
return map[uint]int{}, nil
@@ -36,6 +36,10 @@ func (u *UniformityController) GetAll(c *fiber.Ctx) error {
if err != nil {
return err
}
documents, err := u.UniformityService.MapDocuments(c, result)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.UniformityListDTO]{
@@ -53,7 +57,7 @@ func (u *UniformityController) GetAll(c *fiber.Ctx) error {
"status": "Pengajuan",
},
},
Data: dto.ToUniformityListDTOsWithStandard(result, standards),
Data: dto.ToUniformityListDTOsWithStandard(result, standards, documents),
})
}
@@ -54,6 +54,7 @@ type UniformityDetailDTO struct {
Sampling UniformitySamplingDTO `json:"sampling"`
Result UniformityResultDTO `json:"result"`
Standard *UniformityStandardDTO `json:"standard"`
LatestApproval *approvalDTO.ApprovalRelationDTO `json:"latest_approval"`
UniformityDetails []UniformityDetailItemDTO `json:"uniformity_details"`
}
@@ -63,6 +64,7 @@ type UniformityListDTO struct {
LocationName string `json:"location_name"`
FlockName string `json:"flock_name"`
KandangName string `json:"kandang_name"`
FileName string `json:"file_name"`
AppliedAt *time.Time `json:"applied_at"`
Week int `json:"week"`
Status string `json:"status"`
@@ -115,12 +117,19 @@ func ToUniformityDetailDTO(
info.FileURL = documentURL
}
var latestApproval *approvalDTO.ApprovalRelationDTO
if entityData.LatestApproval != nil {
mapped := approvalDTO.ToApprovalDTO(*entityData.LatestApproval)
latestApproval = &mapped
}
return UniformityDetailDTO{
Id: entityData.Id,
InfoUmum: info,
Sampling: toUniformitySamplingDTO(calc),
Result: toUniformityResultDTO(calc),
Standard: standard,
LatestApproval: latestApproval,
UniformityDetails: toUniformityDetailItemsDTO(calc),
}
}
@@ -163,9 +172,15 @@ func ToUniformityListDTOs(items []entity.ProjectFlockKandangUniformity) []Unifor
func ToUniformityListDTOsWithStandard(
items []entity.ProjectFlockKandangUniformity,
standards map[uint]service.UniformityStandard,
documentNames map[uint]string,
) []UniformityListDTO {
result := ToUniformityListDTOs(items)
if len(result) == 0 || len(standards) == 0 {
for i := range result {
if name, ok := documentNames[result[i].Id]; ok {
result[i].FileName = name
}
}
return result
}
@@ -174,6 +189,9 @@ func ToUniformityListDTOsWithStandard(
result[i].StandardMeanWeight = std.MeanWeight
result[i].StandardUniformity = std.Uniformity
}
if name, ok := documentNames[result[i].Id]; ok {
result[i].FileName = name
}
}
return result
}
@@ -33,6 +33,7 @@ type UniformityService interface {
GetSummary(ctx *fiber.Ctx, id uint) (*entity.ProjectFlockKandangUniformity, error)
GetStandard(ctx *fiber.Ctx, uniformity *entity.ProjectFlockKandangUniformity) (*UniformityStandard, error)
MapStandards(ctx *fiber.Ctx, items []entity.ProjectFlockKandangUniformity) (map[uint]UniformityStandard, 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)
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint, file *multipart.FileHeader, rows []BodyWeightExcelRow) (*entity.ProjectFlockKandangUniformity, error)
DeleteOne(ctx *fiber.Ctx, id uint) error
@@ -189,6 +190,29 @@ func (s uniformityService) MapStandards(c *fiber.Ctx, items []entity.ProjectFloc
return result, nil
}
func (s uniformityService) MapDocuments(c *fiber.Ctx, items []entity.ProjectFlockKandangUniformity) (map[uint]string, error) {
if s.DocumentSvc == nil || len(items) == 0 {
return map[uint]string{}, nil
}
result := make(map[uint]string, len(items))
for _, item := range items {
if item.Id == 0 {
continue
}
documents, err := s.DocumentSvc.ListByTarget(c.Context(), "UNIFORMITY", uint64(item.Id))
if err != nil {
return nil, err
}
if len(documents) == 0 {
continue
}
result[item.Id] = documents[len(documents)-1].Name
}
return result, nil
}
func (s *uniformityService) CreateOne(c *fiber.Ctx, req *validation.Create, file *multipart.FileHeader, rows []BodyWeightExcelRow) (*entity.ProjectFlockKandangUniformity, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
@@ -649,7 +673,7 @@ func (s uniformityService) fetchUniformityDocument(ctx context.Context, uniformi
return nil, "", fiber.NewError(fiber.StatusNotFound, "Uniformity document not found")
}
document := documents[0]
document := documents[len(documents)-1]
url, err := s.DocumentSvc.PresignURL(ctx, document, 15*time.Minute)
if err != nil {
return nil, "", err
@@ -2,6 +2,7 @@ package controller
import (
"math"
"strconv"
"gitlab.com/mbugroup/lti-api.git/internal/modules/repports/dto"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/services"
@@ -95,8 +96,6 @@ func (c *RepportController) GetMarketing(ctx *fiber.Ctx) error {
if err != nil {
return err
}
total := dto.ToSummaryFromDTOItems(result)
return ctx.Status(fiber.StatusOK).
@@ -187,3 +186,44 @@ func (c *RepportController) GetHppPerKandang(ctx *fiber.Ctx) error {
return ctx.Status(fiber.StatusOK).JSON(resp)
}
func (c *RepportController) GetProductionResult(ctx *fiber.Ctx) error {
idParam := ctx.Params("idProjectFlockKandang")
if idParam == "" {
return fiber.NewError(fiber.StatusBadRequest, "idProjectFlockKandang is required")
}
projectFlockKandangID, err := strconv.ParseUint(idParam, 10, 64)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "invalid idProjectFlockKandang")
}
query := &validation.ProductionResultQuery{
Page: ctx.QueryInt("page", 1),
Limit: ctx.QueryInt("limit", 10),
ProjectFlockKandangID: uint(projectFlockKandangID),
}
if query.Page < 1 || query.Limit < 1 {
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
}
data, totalResults, err := c.RepportService.GetProductionResult(ctx, query)
if err != nil {
return err
}
return ctx.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.ProductionResultDTO]{
Code: fiber.StatusOK,
Status: "success",
Message: "Get Laporan Hasil Produksi successfully",
Meta: response.Meta{
Page: query.Page,
Limit: query.Limit,
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
TotalResults: totalResults,
},
Data: data,
})
}
@@ -0,0 +1,43 @@
package dto
import "time"
type ProductionResultDTO struct {
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Woa float64 `json:"woa"`
Bw float64 `json:"bw"`
StdBw float64 `json:"std_bw"`
Uniformity float64 `json:"uniformity"`
StdUniformity string `json:"std_uniformity"`
DepKum float64 `json:"dep_kum"`
DepStd float64 `json:"dep_std"`
ButiranUtuh int64 `json:"butiran_utuh"`
ButiranPutih int64 `json:"butiran_putih"`
ButiranRetak int64 `json:"butiran_retak"`
ButiranPecah int64 `json:"butiran_pecah"`
ButiranJumlah int64 `json:"butiran_jumlah"`
TotalButir int64 `json:"total_butir"`
KgUtuh float64 `json:"kg_utuh"`
KgPutih float64 `json:"kg_putih"`
KgRetak float64 `json:"kg_retak"`
KgPecah float64 `json:"kg_pecah"`
KgJumlah float64 `json:"kg_jumlah"`
TotalKg float64 `json:"total_kg"`
PersenUtuh float64 `json:"persen_utuh"`
PersenPutih float64 `json:"persen_putih"`
PersenRetak float64 `json:"persen_retak"`
PersenPecah float64 `json:"persen_pecah"`
Hd float64 `json:"hd"`
HdStd float64 `json:"hd_std"`
Fi float64 `json:"fi"`
FiStd float64 `json:"fi_std"`
Em float64 `json:"em"`
EmStd float64 `json:"em_std"`
Ew float64 `json:"ew"`
EwStd float64 `json:"ew_std"`
Fcr float64 `json:"fcr"`
FcrStd float64 `json:"fcr_std"`
Hh float64 `json:"hh"`
HhStd float64 `json:"hh_std"`
}
+2 -1
View File
@@ -32,10 +32,11 @@ func (RepportModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
approvalRepository := commonRepo.NewApprovalRepository(db)
purchaseSupplierRepository := repportRepo.NewPurchaseSupplierRepository(db)
hppPerKandangRepository := repportRepo.NewHppPerKandangRepository(db)
productionResultRepository := repportRepo.NewProductionResultRepository(db)
userRepository := rUser.NewUserRepository(db)
approvalSvc := approvalService.NewApprovalService(approvalRepository)
repportService := sRepport.NewRepportService(validate, expenseRealizationRepository, marketingDeliveryProductRepository, purchaseRepository, chickinRepository, recordingRepository, approvalSvc, purchaseSupplierRepository, hppPerKandangRepository)
repportService := sRepport.NewRepportService(validate, expenseRealizationRepository, marketingDeliveryProductRepository, purchaseRepository, chickinRepository, recordingRepository, approvalSvc, purchaseSupplierRepository, hppPerKandangRepository, productionResultRepository)
userService := sUser.NewUserService(userRepository, validate)
RepportRoutes(router, userService, repportService)
@@ -0,0 +1,79 @@
package repositories
import (
"context"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"gorm.io/gorm"
)
type ProductionResultRepository interface {
GetRecordingsByProjectFlockKandang(ctx context.Context, projectFlockKandangID uint, offset, limit int) ([]entity.Recording, int64, error)
}
type productionResultRepositoryImpl struct {
db *gorm.DB
}
func NewProductionResultRepository(db *gorm.DB) ProductionResultRepository {
return &productionResultRepositoryImpl{db: db}
}
func (r *productionResultRepositoryImpl) GetRecordingsByProjectFlockKandang(
ctx context.Context,
projectFlockKandangID uint,
offset, limit int,
) ([]entity.Recording, int64, error) {
if projectFlockKandangID == 0 {
return []entity.Recording{}, 0, nil
}
countQuery := r.db.WithContext(ctx).
Model(&entity.Recording{}).
Where("project_flock_kandangs_id = ?", projectFlockKandangID)
var total int64
if err := countQuery.Count(&total).Error; err != nil {
return nil, 0, err
}
if total == 0 {
return []entity.Recording{}, 0, nil
}
if limit <= 0 {
limit = 10
}
if offset < 0 {
offset = 0
}
flagNames := []string{
string(utils.FlagTelurUtuh),
string(utils.FlagTelurPutih),
string(utils.FlagTelurRetak),
string(utils.FlagTelurPecah),
}
dataQuery := r.db.WithContext(ctx).
Model(&entity.Recording{}).
Where("project_flock_kandangs_id = ?", projectFlockKandangID).
Preload("BodyWeights").
Preload("Eggs", func(db *gorm.DB) *gorm.DB {
return db.Select("recording_eggs.*, f.name AS product_flag_name").
Joins("LEFT JOIN product_warehouses pw ON pw.id = recording_eggs.product_warehouse_id").
Joins("LEFT JOIN flags f ON f.flagable_id = pw.product_id AND f.flagable_type = ? AND f.name IN ?", entity.FlagableTypeProduct, flagNames)
}).
Preload("Eggs.ProductWarehouse").
Order("record_datetime ASC").
Offset(offset).
Limit(limit)
var recordings []entity.Recording
if err := dataQuery.Find(&recordings).Error; err != nil {
return nil, 0, err
}
return recordings, total, nil
}
+2 -1
View File
@@ -18,6 +18,7 @@ func RepportRoutes(v1 fiber.Router, u user.UserService, s repport.RepportService
route.Get("/expense", m.RequirePermissions(m.P_ReportExpenseGetAll), ctrl.GetExpense)
route.Get("/marketing", m.RequirePermissions(m.P_ReportDeliveryGetAll), ctrl.GetMarketing)
route.Get("/purchase-supplier", m.RequirePermissions(m.P_ReportPurchaseSupplierGetAll), ctrl.GetPurchaseSupplier)
route.Get("/hpp-per-kandang", m.RequirePermissions(m.P_ReportHppPerKandangGetAll),ctrl.GetHppPerKandang)
route.Get("/hpp-per-kandang", ctrl.GetHppPerKandang)
route.Get("/production-result/:idProjectFlockKandang", ctrl.GetProductionResult)
}
@@ -35,6 +35,7 @@ type RepportService interface {
GetMarketing(ctx *fiber.Ctx, params *validation.MarketingQuery) ([]dto.RepportMarketingItemDTO, int64, error)
GetPurchaseSupplier(ctx *fiber.Ctx, params *validation.PurchaseSupplierQuery) ([]dto.PurchaseSupplierDTO, int64, error)
GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangResponseData, *dto.HppPerKandangMetaDTO, error)
GetProductionResult(ctx *fiber.Ctx, params *validation.ProductionResultQuery) ([]dto.ProductionResultDTO, int64, error)
}
type repportService struct {
@@ -48,6 +49,7 @@ type repportService struct {
ApprovalSvc approvalService.ApprovalService
PurchaseSupplierRepo repportRepo.PurchaseSupplierRepository
HppPerKandangRepo repportRepo.HppPerKandangRepository
ProductionResultRepo repportRepo.ProductionResultRepository
}
type HppCostAggregate struct {
@@ -69,6 +71,7 @@ func NewRepportService(
approvalSvc approvalService.ApprovalService,
purchaseSupplierRepo repportRepo.PurchaseSupplierRepository,
hppPerKandangRepo repportRepo.HppPerKandangRepository,
productionResultRepo repportRepo.ProductionResultRepository,
) RepportService {
return &repportService{
Log: utils.Log,
@@ -81,6 +84,7 @@ func NewRepportService(
ApprovalSvc: approvalSvc,
PurchaseSupplierRepo: purchaseSupplierRepo,
HppPerKandangRepo: hppPerKandangRepo,
ProductionResultRepo: productionResultRepo,
}
}
@@ -229,6 +233,352 @@ func (s *repportService) getTotalProjectCost(ctx context.Context, projectFlockID
return cost
}
func (s *repportService) GetProductionResult(ctx *fiber.Ctx, params *validation.ProductionResultQuery) ([]dto.ProductionResultDTO, int64, error) {
if err := s.Validate.Struct(params); err != nil {
return nil, 0, err
}
const (
recordsPerWeek = 7
defaultStartWoa = 18
defaultStdBw = 1951
defaultBw = 0
defaultUniformText = "90% up"
)
if params.Limit <= 0 {
params.Limit = 10
}
if params.Page <= 0 {
params.Page = 1
}
weeksPerPage := params.Limit
recordLimit := weeksPerPage * recordsPerWeek
if recordLimit <= 0 {
recordLimit = recordsPerWeek
}
recordOffset := (params.Page - 1) * recordLimit
if recordOffset < 0 {
recordOffset = 0
}
recordings, totalRecordings, err := s.ProductionResultRepo.GetRecordingsByProjectFlockKandang(ctx.Context(), params.ProjectFlockKandangID, recordOffset, recordLimit)
if err != nil {
return nil, 0, err
}
dailyResults := make([]dto.ProductionResultDTO, len(recordings))
for i := range recordings {
dailyResults[i] = mapRecordingToProductionResultDTO(recordings[i])
if dailyResults[i].StdUniformity == "" {
dailyResults[i].StdUniformity = defaultUniformText
}
}
weeklyResults := summarizeProductionResults(dailyResults, recordsPerWeek)
var cumulativeButir int64
var cumulativeKg float64
for i := range weeklyResults {
weeklyResults[i].Woa = float64(defaultStartWoa + i)
weeklyResults[i].StdBw = defaultStdBw
weeklyResults[i].Bw = defaultBw
if weeklyResults[i].StdUniformity == "" {
weeklyResults[i].StdUniformity = defaultUniformText
}
cumulativeButir += weeklyResults[i].ButiranJumlah
weeklyResults[i].TotalButir = cumulativeButir
cumulativeKg += weeklyResults[i].KgJumlah
weeklyResults[i].TotalKg = cumulativeKg
}
totalWeeks := int64(math.Ceil(float64(totalRecordings) / float64(recordsPerWeek)))
return weeklyResults, totalWeeks, nil
}
func mapRecordingToProductionResultDTO(record entity.Recording) dto.ProductionResultDTO {
result := dto.ProductionResultDTO{
CreatedAt: record.CreatedAt,
UpdatedAt: record.UpdatedAt,
StdUniformity: "90% up",
DepKum: valueOrZero(record.CumDepletionRate),
DepStd: valueOrZero(record.TotalDepletionQty),
Fcr: valueOrZero(record.FcrValue),
Hh: valueOrZero(record.TotalChickQty),
}
if record.Day != nil {
result.Woa = float64(*record.Day)
}
if record.CumIntake != nil {
result.Fi = float64(*record.CumIntake)
}
// avgWeight := calculateAverageBodyWeight(record.BodyWeights)
avgWeight := 1.0
if avgWeight > 0 {
result.Bw = avgWeight
}
eggSummary := summarizeEggs(record.Eggs)
result.ButiranUtuh = eggSummary.Utuh
result.ButiranPutih = eggSummary.Putih
result.ButiranRetak = eggSummary.Retak
result.ButiranPecah = eggSummary.Pecah
result.ButiranJumlah = eggSummary.TotalQty
result.TotalButir = eggSummary.TotalQty
result.KgUtuh = eggSummary.KgUtuh
result.KgPutih = eggSummary.KgPutih
result.KgRetak = eggSummary.KgRetak
result.KgPecah = eggSummary.KgPecah
result.KgJumlah = eggSummary.TotalKg
result.TotalKg = eggSummary.TotalKg
if eggSummary.TotalQty > 0 {
total := float64(eggSummary.TotalQty)
result.PersenUtuh = roundFloat((float64(result.ButiranUtuh)/total)*100, 2)
result.PersenPutih = roundFloat((float64(result.ButiranPutih)/total)*100, 2)
result.PersenRetak = roundFloat((float64(result.ButiranRetak)/total)*100, 2)
result.PersenPecah = roundFloat((float64(result.ButiranPecah)/total)*100, 2)
result.Ew = (eggSummary.TotalKg * 1000) / total
result.Em = eggSummary.TotalKg
}
return result
}
// func calculateAverageBodyWeight(bodyWeights []entity.RecordingBW) float64 {
// var totalQty float64
// var totalWeight float64
// for _, bw := range bodyWeights {
// totalQty += bw.Qty
// if bw.TotalWeight > 0 {
// totalWeight += bw.TotalWeight
// } else {
// totalWeight += bw.AvgWeight * bw.Qty
// }
// }
// if totalQty == 0 {
// return 0
// }
// return totalWeight / totalQty
// }
type eggSummary struct {
TotalQty int64
TotalKg float64
Utuh int64
Putih int64
Retak int64
Pecah int64
KgUtuh float64
KgPutih float64
KgRetak float64
KgPecah float64
}
func summarizeEggs(eggs []entity.RecordingEgg) eggSummary {
var summary eggSummary
for _, egg := range eggs {
qty := int64(egg.Qty)
weightKg := valueOrZero(egg.Weight)
summary.TotalQty += qty
summary.TotalKg += weightKg
if flagType, ok := getEggFlagType(egg); ok {
switch flagType {
case utils.FlagTelurUtuh:
summary.Utuh += qty
summary.KgUtuh += weightKg
case utils.FlagTelurPutih:
summary.Putih += qty
summary.KgPutih += weightKg
case utils.FlagTelurRetak:
summary.Retak += qty
summary.KgRetak += weightKg
case utils.FlagTelurPecah:
summary.Pecah += qty
summary.KgPecah += weightKg
}
}
}
return summary
}
func valueOrZero(value *float64) float64 {
if value == nil {
return 0
}
return *value
}
func roundFloat(val float64, precision int) float64 {
if precision < 0 {
return val
}
factor := math.Pow(10, float64(precision))
return math.Round(val*factor) / factor
}
func getEggFlagType(egg entity.RecordingEgg) (utils.FlagType, bool) {
if egg.ProductFlagName == nil || *egg.ProductFlagName == "" {
return "", false
}
flagType := utils.FlagType(*egg.ProductFlagName)
switch flagType {
case utils.FlagTelurUtuh, utils.FlagTelurPutih, utils.FlagTelurRetak, utils.FlagTelurPecah:
return flagType, true
}
return "", false
}
func summarizeProductionResults(daily []dto.ProductionResultDTO, groupSize int) []dto.ProductionResultDTO {
if groupSize <= 0 || len(daily) == 0 {
return daily
}
result := make([]dto.ProductionResultDTO, 0, (len(daily)+groupSize-1)/groupSize)
for i := 0; i < len(daily); i += groupSize {
end := i + groupSize
if end > len(daily) {
end = len(daily)
}
result = append(result, aggregateProductionResultGroup(daily[i:end]))
}
return result
}
func aggregateProductionResultGroup(group []dto.ProductionResultDTO) dto.ProductionResultDTO {
count := len(group)
if count == 0 {
return dto.ProductionResultDTO{}
}
agg := dto.ProductionResultDTO{
CreatedAt: group[0].CreatedAt,
UpdatedAt: group[0].UpdatedAt,
StdUniformity: group[0].StdUniformity,
}
var sumBw, sumStdBw, sumUniformity float64
var sumDepStd float64
var sumKgUtuh, sumKgPutih, sumKgRetak, sumKgPecah float64
var sumKgJumlah, sumTotalKg float64
var sumPersenUtuh, sumPersenPutih, sumPersenRetak, sumPersenPecah float64
var percentSamples int
var sumHd, sumHdStd float64
var sumFi, sumFiStd float64
var sumEm, sumEmStd float64
var sumEw, sumEwStd float64
var sumFcr, sumFcrStd float64
var sumHh, sumHhStd float64
var sumButiranUtuh, sumButiranPutih int64
var sumButiranRetak, sumButiranPecah int64
var sumButiranJumlah, sumTotalButir int64
for _, item := range group {
sumBw += item.Bw
sumStdBw += item.StdBw
sumUniformity += item.Uniformity
sumDepStd += item.DepStd
sumKgUtuh += item.KgUtuh
sumKgPutih += item.KgPutih
sumKgRetak += item.KgRetak
sumKgPecah += item.KgPecah
sumKgJumlah += item.KgJumlah
sumTotalKg += item.TotalKg
if item.ButiranJumlah > 0 {
sumPersenUtuh += item.PersenUtuh
sumPersenPutih += item.PersenPutih
sumPersenRetak += item.PersenRetak
sumPersenPecah += item.PersenPecah
percentSamples++
}
sumHd += item.Hd
sumHdStd += item.HdStd
sumFi += item.Fi
sumFiStd += item.FiStd
sumEm += item.Em
sumEmStd += item.EmStd
sumEw += item.Ew
sumEwStd += item.EwStd
sumFcr += item.Fcr
sumFcrStd += item.FcrStd
sumHh += item.Hh
sumHhStd += item.HhStd
sumButiranUtuh += item.ButiranUtuh
sumButiranPutih += item.ButiranPutih
sumButiranRetak += item.ButiranRetak
sumButiranPecah += item.ButiranPecah
sumButiranJumlah += item.ButiranJumlah
sumTotalButir += item.TotalButir
}
divider := float64(count)
if divider == 0 {
divider = 1
}
agg.Bw = sumBw / divider
agg.StdBw = sumStdBw / divider
agg.Uniformity = sumUniformity / divider
agg.DepKum = group[count-1].DepKum
agg.DepStd = sumDepStd / divider
agg.KgUtuh = sumKgUtuh
agg.KgPutih = sumKgPutih
agg.KgRetak = sumKgRetak
agg.KgPecah = sumKgPecah
agg.KgJumlah = sumKgJumlah
agg.TotalKg = sumTotalKg
agg.ButiranUtuh = sumButiranUtuh
agg.ButiranPutih = sumButiranPutih
agg.ButiranRetak = sumButiranRetak
agg.ButiranPecah = sumButiranPecah
agg.ButiranJumlah = sumButiranJumlah
agg.TotalButir = sumTotalButir
if percentSamples > 0 {
percentDivider := float64(percentSamples)
agg.PersenUtuh = roundFloat(sumPersenUtuh/percentDivider, 2)
agg.PersenPutih = roundFloat(sumPersenPutih/percentDivider, 2)
agg.PersenRetak = roundFloat(sumPersenRetak/percentDivider, 2)
agg.PersenPecah = roundFloat(sumPersenPecah/percentDivider, 2)
}
agg.Hd = sumHd / divider
agg.HdStd = sumHdStd / divider
agg.Fi = sumFi / divider
agg.FiStd = sumFiStd / divider
agg.Em = sumEm / divider
agg.EmStd = sumEmStd / divider
agg.Ew = sumEw / divider
agg.EwStd = sumEwStd / divider
agg.Fcr = sumFcr / divider
agg.FcrStd = sumFcrStd / divider
agg.Hh = sumHh / divider
agg.HhStd = sumHhStd / divider
return agg
}
func (s *repportService) GetPurchaseSupplier(c *fiber.Ctx, params *validation.PurchaseSupplierQuery) ([]dto.PurchaseSupplierDTO, int64, error) {
if err := s.Validate.Struct(params); err != nil {
return nil, 0, err
@@ -54,3 +54,9 @@ type HppPerKandangQuery struct {
WeightMin *float64 `query:"-"`
WeightMax *float64 `query:"-"`
}
type ProductionResultQuery struct {
Page int `query:"page" validate:"omitempty,min=1,gt=0"`
Limit int `query:"limit" validate:"omitempty,min=1,max=100,gt=0"`
ProjectFlockKandangID uint `query:"-" validate:"required,gt=0"`
}
@@ -144,6 +144,9 @@ func (h *Controller) Refresh(c *fiber.Ctx) error {
refreshName := resolveSSOCookieName(config.SSORefreshCookieName, "refresh")
refreshToken := strings.TrimSpace(c.Cookies(refreshName))
if refreshToken == "" {
if target := buildStartRedirect(defaultSSOClientAlias()); target != "" {
return c.Redirect(target, fiber.StatusFound)
}
return fiber.NewError(fiber.StatusUnauthorized, "unauthenticated")
}
@@ -174,6 +177,9 @@ func (h *Controller) Refresh(c *fiber.Ctx) error {
if resp.StatusCode == fiber.StatusTooManyRequests {
return fiber.NewError(fiber.StatusTooManyRequests, "Too many attempts, please slow down")
}
if target := buildStartRedirect(defaultSSOClientAlias()); target != "" {
return c.Redirect(target, fiber.StatusFound)
}
return fiber.NewError(fiber.StatusUnauthorized, "unauthenticated")
}
@@ -425,6 +431,7 @@ func (h *Controller) Logout(c *fiber.Ctx) error {
refreshName := resolveSSOCookieName(config.SSORefreshCookieName, "refresh")
var accessToken, refreshToken string
var verification *sso.VerificationResult
if accessName != "" {
accessToken = strings.TrimSpace(c.Cookies(accessName))
}
@@ -446,9 +453,10 @@ func (h *Controller) Logout(c *fiber.Ctx) error {
}
if hadAccessCookie {
if verification, err := sso.VerifyAccessToken(accessToken); err != nil {
if v, err := sso.VerifyAccessToken(accessToken); err != nil {
utils.Log.WithError(err).Warn("failed to verify access token during logout")
} else {
verification = v
if revoker := session.GetRevocationStore(); revoker != nil {
if err := revoker.MarkUserLogout(c.Context(), verification.UserID, time.Now().UTC()); err != nil {
utils.Log.WithError(err).Warn("failed to mark user logout")
@@ -475,6 +483,28 @@ func (h *Controller) Logout(c *fiber.Ctx) error {
} else if rawReturn != "" {
utils.Log.WithError(err).Warn("invalid return_to during logout")
}
} else if rawReturn == "" && config.SSOPortalURL != "" {
if alias, singleCfg, ok := singleClientFromToken(verification); ok {
if normalized, err := normalizeReturnTarget(singleCfg.DefaultReturnURI, singleCfg); err == nil && normalized != "" {
redirectTarget = normalized
alias, cfg, hasClientInfo = alias, singleCfg, true
} else {
redirectTarget = config.SSOPortalURL
}
} else if accessToken != "" {
if alias, singleCfg, ok := h.singleClientFromSSO(c.Context(), accessToken); ok {
if normalized, err := normalizeReturnTarget(singleCfg.DefaultReturnURI, singleCfg); err == nil && normalized != "" {
redirectTarget = normalized
alias, cfg, hasClientInfo = alias, singleCfg, true
} else {
redirectTarget = config.SSOPortalURL
}
} else {
redirectTarget = config.SSOPortalURL
}
} else {
redirectTarget = config.SSOPortalURL
}
} else if rawReturn != "" {
if strings.HasPrefix(rawReturn, "/") && !strings.HasPrefix(rawReturn, "//") {
redirectTarget = rawReturn
@@ -494,6 +524,177 @@ func (h *Controller) Logout(c *fiber.Ctx) error {
return c.Status(fiber.StatusOK).JSON(fiber.Map{"status": "signed out"})
}
func singleSSOClient() (string, config.SSOClientConfig, bool) {
if len(config.SSOClients) != 1 {
return "", config.SSOClientConfig{}, false
}
for alias, cfg := range config.SSOClients {
if strings.TrimSpace(alias) == "" || strings.TrimSpace(cfg.PublicID) == "" {
return "", config.SSOClientConfig{}, false
}
return alias, cfg, true
}
return "", config.SSOClientConfig{}, false
}
func singleClientFromToken(verification *sso.VerificationResult) (string, config.SSOClientConfig, bool) {
if verification == nil || verification.Claims == nil {
return "", config.SSOClientConfig{}, false
}
return singleClientFromScopes(verification.Claims.Scopes())
}
func (h *Controller) singleClientFromSSO(ctx context.Context, accessToken string) (string, config.SSOClientConfig, bool) {
accessToken = strings.TrimSpace(accessToken)
if accessToken == "" {
return "", config.SSOClientConfig{}, false
}
meURL := strings.TrimSpace(config.SSOGetMeURL)
if meURL == "" {
return "", config.SSOClientConfig{}, false
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, meURL, nil)
if err != nil {
utils.Log.WithError(err).Warn("failed to build SSO getme request")
return "", config.SSOClientConfig{}, false
}
req.Header.Set("Authorization", "Bearer "+accessToken)
resp, err := h.httpClient.Do(req)
if err != nil {
utils.Log.WithError(err).Warn("SSO getme request failed")
return "", config.SSOClientConfig{}, false
}
defer resp.Body.Close()
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
utils.Log.WithField("status", resp.StatusCode).Warn("SSO getme responded with error")
return "", config.SSOClientConfig{}, false
}
var payload struct {
Data struct {
Roles []struct {
Client *struct {
Alias string `json:"alias"`
} `json:"client"`
} `json:"roles"`
} `json:"data"`
}
if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil {
utils.Log.WithError(err).Warn("failed to decode SSO getme response")
return "", config.SSOClientConfig{}, false
}
aliases := make(map[string]struct{})
for _, role := range payload.Data.Roles {
if role.Client == nil {
continue
}
alias := strings.ToLower(strings.TrimSpace(role.Client.Alias))
if alias != "" {
aliases[alias] = struct{}{}
}
}
if len(aliases) != 1 {
return "", config.SSOClientConfig{}, false
}
for alias := range aliases {
if normalized, cfg, ok := findClientAlias(alias); ok {
return normalized, cfg, true
}
return "", config.SSOClientConfig{}, false
}
return "", config.SSOClientConfig{}, false
}
func singleClientFromScopes(scopes []string) (string, config.SSOClientConfig, bool) {
if len(scopes) == 0 {
return "", config.SSOClientConfig{}, false
}
seen := make(map[string]struct{})
for _, scope := range scopes {
if alias, ok := matchClientAliasFromScope(scope); ok {
seen[alias] = struct{}{}
}
if len(seen) > 1 {
return "", config.SSOClientConfig{}, false
}
}
if len(seen) != 1 {
return "", config.SSOClientConfig{}, false
}
for alias := range seen {
if normalized, cfg, ok := findClientAlias(alias); ok {
return normalized, cfg, true
}
}
return "", config.SSOClientConfig{}, false
}
func matchClientAliasFromScope(scope string) (string, bool) {
scope = strings.ToLower(strings.TrimSpace(scope))
if scope == "" {
return "", false
}
prefix := scope
if idx := strings.IndexAny(prefix, ".:"); idx > 0 {
prefix = prefix[:idx]
}
if prefix == "" {
return "", false
}
if alias, _, ok := findClientAlias(prefix); ok {
return alias, true
}
if prefix == "user-management" {
if alias, _, ok := findClientAlias("umgmt"); ok {
return alias, true
}
}
if prefix == "umgmt" {
if alias, _, ok := findClientAlias("user-management"); ok {
return alias, true
}
}
return "", false
}
func findClientAlias(alias string) (string, config.SSOClientConfig, bool) {
alias = strings.TrimSpace(alias)
if alias == "" {
return "", config.SSOClientConfig{}, false
}
if cfg, ok := config.SSOClients[alias]; ok && strings.TrimSpace(cfg.PublicID) != "" {
return alias, cfg, true
}
for key, cfg := range config.SSOClients {
if strings.EqualFold(key, alias) && strings.TrimSpace(cfg.PublicID) != "" {
return key, cfg, true
}
}
return "", config.SSOClientConfig{}, false
}
func defaultSSOClientAlias() string {
for alias := range config.SSOClients {
if strings.TrimSpace(alias) == "" {
continue
}
return alias
}
return ""
}
func buildStartRedirect(alias string) string {
alias = strings.TrimSpace(alias)
if alias == "" {
return ""
}
return "/api/sso/start?client=" + url.QueryEscape(alias)
}
func (h *Controller) revokeToken(ctx context.Context, token string, verification *sso.VerificationResult) {
if h.revoker == nil || verification == nil || verification.Claims == nil {
return