diff --git a/internal/database/migrations/20260107120000_create_config_checklists_table.down.sql b/internal/database/migrations/20260107120000_create_config_checklists_table.down.sql new file mode 100644 index 00000000..e4c4d3dd --- /dev/null +++ b/internal/database/migrations/20260107120000_create_config_checklists_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS config_checklists; diff --git a/internal/database/migrations/20260107120000_create_config_checklists_table.up.sql b/internal/database/migrations/20260107120000_create_config_checklists_table.up.sql new file mode 100644 index 00000000..57589f31 --- /dev/null +++ b/internal/database/migrations/20260107120000_create_config_checklists_table.up.sql @@ -0,0 +1,9 @@ +CREATE TABLE IF NOT EXISTS config_checklists ( + id BIGSERIAL PRIMARY KEY, + date DATE NOT NULL, + percentage_threshold_bad INTEGER NOT NULL, + percentage_threshold_enough INTEGER NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + deleted_at TIMESTAMPTZ +); diff --git a/internal/entities/config-checklist.go b/internal/entities/config-checklist.go new file mode 100644 index 00000000..563d88de --- /dev/null +++ b/internal/entities/config-checklist.go @@ -0,0 +1,17 @@ +package entities + +import ( + "time" + + "gorm.io/gorm" +) + +type ConfigChecklist struct { + Id uint `gorm:"primaryKey"` + Date time.Time `gorm:"type:date;not null"` + PercentageThresholdBad int `gorm:"not null"` + PercentageThresholdEnough int `gorm:"not null"` + CreatedAt time.Time `gorm:"autoCreateTime"` + UpdatedAt time.Time `gorm:"autoUpdateTime"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` +} diff --git a/internal/modules/daily-checklists/services/daily-checklist.service.go b/internal/modules/daily-checklists/services/daily-checklist.service.go index 3f23f6a3..2ed15fad 100644 --- a/internal/modules/daily-checklists/services/daily-checklist.service.go +++ b/internal/modules/daily-checklists/services/daily-checklist.service.go @@ -951,6 +951,12 @@ func (s dailyChecklistService) GetReport(c *fiber.Ctx, params *validation.Report PhaseID uint } + type dailyActivityStat struct { + Completed int + Total int + Date time.Time + } + employeeIDs := make([]uint, 0) kandangIDs := make([]uint, 0) phaseIDs := make([]uint, 0) @@ -976,7 +982,7 @@ func (s dailyChecklistService) GetReport(c *fiber.Ctx, params *validation.Report } } - dailyActivityMap := make(map[comboKey]map[string]int) + dailyActivityMap := make(map[comboKey]map[string]dailyActivityStat) if len(employeeIDs) > 0 { var dailyRows []struct { EmployeeID uint @@ -984,6 +990,7 @@ func (s dailyChecklistService) GetReport(c *fiber.Ctx, params *validation.Report PhaseID uint Date time.Time Completed int64 + Total int64 } dailyQuery := buildBase(). @@ -995,7 +1002,8 @@ func (s dailyChecklistService) GetReport(c *fiber.Ctx, params *validation.Report dc.kandang_id, dcat.phase_id, dc.date, - SUM(CASE WHEN dca.checked THEN 1 ELSE 0 END) AS completed`). + SUM(CASE WHEN dca.checked THEN 1 ELSE 0 END) AS completed, + COUNT(*) AS total`). Group("dca.employee_id, dc.kandang_id, dcat.phase_id, dc.date") if err := dailyQuery.Scan(&dailyRows).Error; err != nil { @@ -1009,10 +1017,14 @@ func (s dailyChecklistService) GetReport(c *fiber.Ctx, params *validation.Report continue } if _, ok := dailyActivityMap[key]; !ok { - dailyActivityMap[key] = make(map[string]int) + dailyActivityMap[key] = make(map[string]dailyActivityStat) } day := strconv.Itoa(row.Date.Day()) - dailyActivityMap[key][day] = int(row.Completed) + dailyActivityMap[key][day] = dailyActivityStat{ + Completed: int(row.Completed), + Total: int(row.Total), + Date: row.Date, + } } } @@ -1068,18 +1080,66 @@ func (s dailyChecklistService) GetReport(c *fiber.Ctx, params *validation.Report }{Completed: row.Completed, Total: row.Total} } + var configs []entity.ConfigChecklist + if err := s.Repository.DB().WithContext(c.Context()). + Order("date ASC"). + Find(&configs).Error; err != nil { + s.Log.Errorf("Failed to load config checklists: %+v", err) + return nil, 0, err + } + + getConfigForDate := func(date time.Time) *entity.ConfigChecklist { + var selected *entity.ConfigChecklist + for i := range configs { + if !configs[i].Date.After(date) { + selected = &configs[i] + } else { + break + } + } + if selected == nil { + return &entity.ConfigChecklist{ + PercentageThresholdBad: 50, + PercentageThresholdEnough: 75, + } + } + return selected + } + items := make([]DailyChecklistReportItem, len(rows)) for i, row := range rows { key := comboKey{EmployeeID: row.EmployeeID, KandangID: row.KandangID, PhaseID: row.PhaseID} activities := dailyActivityMap[key] if activities == nil { - activities = map[string]int{} + activities = map[string]dailyActivityStat{} } totalChecklist := 0 - for _, count := range activities { - totalChecklist += count + categoryCounts := DailyChecklistReportCategory{} + activityOutput := make(map[string]int, len(activities)) + + for day, stat := range activities { + activityOutput[day] = stat.Completed + totalChecklist += stat.Completed + + if stat.Total == 0 { + continue + } + + cfg := getConfigForDate(stat.Date) + if cfg == nil { + continue + } + + progress := int(math.Ceil(float64(stat.Completed) / float64(stat.Total) * 100)) + if progress <= cfg.PercentageThresholdBad { + categoryCounts.Kurang++ + } else if progress <= cfg.PercentageThresholdEnough { + categoryCounts.Cukup++ + } else { + categoryCounts.Baik++ + } } employeeStat := employeeStats[row.EmployeeID] @@ -1104,17 +1164,13 @@ func (s dailyChecklistService) GetReport(c *fiber.Ctx, params *validation.Report EmployeeID: row.EmployeeID, EmployeeName: row.EmployeeName, PhaseName: row.PhaseName, - DailyActivities: activities, + DailyActivities: activityOutput, Summary: DailyChecklistReportSummary{ TotalChecklist: totalChecklist, JumlahHariEfektif: len(activities), AbkPercentage: abkPercentage, KandangPercentage: kandangPercentage, - Category: DailyChecklistReportCategory{ - Kurang: 0, - Cukup: 0, - Baik: 0, - }, + Category: categoryCounts, }, } } diff --git a/internal/modules/master/config-checklists/controllers/config-checklist.controller.go b/internal/modules/master/config-checklists/controllers/config-checklist.controller.go new file mode 100644 index 00000000..362f1aaa --- /dev/null +++ b/internal/modules/master/config-checklists/controllers/config-checklist.controller.go @@ -0,0 +1,144 @@ +package controller + +import ( + "math" + "strconv" + + "gitlab.com/mbugroup/lti-api.git/internal/modules/master/config-checklists/dto" + service "gitlab.com/mbugroup/lti-api.git/internal/modules/master/config-checklists/services" + validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/config-checklists/validations" + "gitlab.com/mbugroup/lti-api.git/internal/response" + + "github.com/gofiber/fiber/v2" +) + +type ConfigChecklistController struct { + ConfigChecklistService service.ConfigChecklistService +} + +func NewConfigChecklistController(configChecklistService service.ConfigChecklistService) *ConfigChecklistController { + return &ConfigChecklistController{ + ConfigChecklistService: configChecklistService, + } +} + +func (u *ConfigChecklistController) GetAll(c *fiber.Ctx) error { + query := &validation.Query{ + Page: c.QueryInt("page", 1), + Limit: c.QueryInt("limit", 10), + Search: c.Query("search", ""), + } + + if query.Page < 1 || query.Limit < 1 { + return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0") + } + + result, totalResults, err := u.ConfigChecklistService.GetAll(c, query) + if err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.SuccessWithPaginate[dto.ConfigChecklistListDTO]{ + Code: fiber.StatusOK, + Status: "success", + Message: "Get all configChecklists successfully", + Meta: response.Meta{ + Page: query.Page, + Limit: query.Limit, + TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))), + TotalResults: totalResults, + }, + Data: dto.ToConfigChecklistListDTOs(result), + }) +} + +func (u *ConfigChecklistController) GetOne(c *fiber.Ctx) error { + param := c.Params("id") + + id, err := strconv.Atoi(param) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid Id") + } + + result, err := u.ConfigChecklistService.GetOne(c, uint(id)) + if err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.Success{ + Code: fiber.StatusOK, + Status: "success", + Message: "Get configChecklist successfully", + Data: dto.ToConfigChecklistListDTO(*result), + }) +} + +func (u *ConfigChecklistController) CreateOne(c *fiber.Ctx) error { + req := new(validation.Create) + + if err := c.BodyParser(req); err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") + } + + result, err := u.ConfigChecklistService.CreateOne(c, req) + if err != nil { + return err + } + + return c.Status(fiber.StatusCreated). + JSON(response.Success{ + Code: fiber.StatusCreated, + Status: "success", + Message: "Create configChecklist successfully", + Data: dto.ToConfigChecklistListDTO(*result), + }) +} + +func (u *ConfigChecklistController) UpdateOne(c *fiber.Ctx) error { + req := new(validation.Update) + param := c.Params("id") + + id, err := strconv.Atoi(param) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid Id") + } + + if err := c.BodyParser(req); err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") + } + + result, err := u.ConfigChecklistService.UpdateOne(c, req, uint(id)) + if err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.Success{ + Code: fiber.StatusOK, + Status: "success", + Message: "Update configChecklist successfully", + Data: dto.ToConfigChecklistListDTO(*result), + }) +} + +func (u *ConfigChecklistController) DeleteOne(c *fiber.Ctx) error { + param := c.Params("id") + + id, err := strconv.Atoi(param) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid Id") + } + + if err := u.ConfigChecklistService.DeleteOne(c, uint(id)); err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.Common{ + Code: fiber.StatusOK, + Status: "success", + Message: "Delete configChecklist successfully", + }) +} diff --git a/internal/modules/master/config-checklists/dto/config-checklist.dto.go b/internal/modules/master/config-checklists/dto/config-checklist.dto.go new file mode 100644 index 00000000..d6af71aa --- /dev/null +++ b/internal/modules/master/config-checklists/dto/config-checklist.dto.go @@ -0,0 +1,61 @@ +package dto + +import ( + "time" + + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" +) + +// === DTO Structs === + +type ConfigChecklistRelationDTO struct { + Id uint `json:"id"` + Date time.Time `json:"date"` +} + +type ConfigChecklistListDTO struct { + Id uint `json:"id"` + Date time.Time `json:"date"` + PercentageThresholdBad int `json:"percentage_threshold_bad"` + PercentageThresholdEnough int `json:"percentage_threshold_enough"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type ConfigChecklistDetailDTO struct { + ConfigChecklistListDTO +} + +// === Mapper Functions === + +func ToConfigChecklistRelationDTO(e entity.ConfigChecklist) ConfigChecklistRelationDTO { + return ConfigChecklistRelationDTO{ + Id: e.Id, + Date: e.Date, + } +} + +func ToConfigChecklistListDTO(e entity.ConfigChecklist) ConfigChecklistListDTO { + return ConfigChecklistListDTO{ + Id: e.Id, + Date: e.Date, + PercentageThresholdBad: e.PercentageThresholdBad, + PercentageThresholdEnough: e.PercentageThresholdEnough, + CreatedAt: e.CreatedAt, + UpdatedAt: e.UpdatedAt, + } +} + +func ToConfigChecklistListDTOs(e []entity.ConfigChecklist) []ConfigChecklistListDTO { + result := make([]ConfigChecklistListDTO, len(e)) + for i, r := range e { + result[i] = ToConfigChecklistListDTO(r) + } + return result +} + +func ToConfigChecklistDetailDTO(e entity.ConfigChecklist) ConfigChecklistDetailDTO { + return ConfigChecklistDetailDTO{ + ConfigChecklistListDTO: ToConfigChecklistListDTO(e), + } +} diff --git a/internal/modules/master/config-checklists/module.go b/internal/modules/master/config-checklists/module.go new file mode 100644 index 00000000..711a91f3 --- /dev/null +++ b/internal/modules/master/config-checklists/module.go @@ -0,0 +1,25 @@ +package configChecklists + +import ( + "github.com/go-playground/validator/v10" + "github.com/gofiber/fiber/v2" + "gorm.io/gorm" + + rConfigChecklist "gitlab.com/mbugroup/lti-api.git/internal/modules/master/config-checklists/repositories" + sConfigChecklist "gitlab.com/mbugroup/lti-api.git/internal/modules/master/config-checklists/services" + + rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" + sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" +) + +type ConfigChecklistModule struct{} + +func (ConfigChecklistModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) { + configChecklistRepo := rConfigChecklist.NewConfigChecklistRepository(db) + userRepo := rUser.NewUserRepository(db) + + configChecklistService := sConfigChecklist.NewConfigChecklistService(configChecklistRepo, validate) + userService := sUser.NewUserService(userRepo, validate) + + ConfigChecklistRoutes(router, userService, configChecklistService) +} diff --git a/internal/modules/master/config-checklists/repositories/config-checklist.repository.go b/internal/modules/master/config-checklists/repositories/config-checklist.repository.go new file mode 100644 index 00000000..5bbf75ca --- /dev/null +++ b/internal/modules/master/config-checklists/repositories/config-checklist.repository.go @@ -0,0 +1,21 @@ +package repository + +import ( + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + "gitlab.com/mbugroup/lti-api.git/internal/common/repository" + "gorm.io/gorm" +) + +type ConfigChecklistRepository interface { + repository.BaseRepository[entity.ConfigChecklist] +} + +type ConfigChecklistRepositoryImpl struct { + *repository.BaseRepositoryImpl[entity.ConfigChecklist] +} + +func NewConfigChecklistRepository(db *gorm.DB) ConfigChecklistRepository { + return &ConfigChecklistRepositoryImpl{ + BaseRepositoryImpl: repository.NewBaseRepository[entity.ConfigChecklist](db), + } +} diff --git a/internal/modules/master/config-checklists/route.go b/internal/modules/master/config-checklists/route.go new file mode 100644 index 00000000..1b590067 --- /dev/null +++ b/internal/modules/master/config-checklists/route.go @@ -0,0 +1,23 @@ +package configChecklists + +import ( + m "gitlab.com/mbugroup/lti-api.git/internal/middleware" + controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/config-checklists/controllers" + configChecklist "gitlab.com/mbugroup/lti-api.git/internal/modules/master/config-checklists/services" + user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" + + "github.com/gofiber/fiber/v2" +) + +func ConfigChecklistRoutes(v1 fiber.Router, u user.UserService, s configChecklist.ConfigChecklistService) { + ctrl := controller.NewConfigChecklistController(s) + + route := v1.Group("/config-checklists") + route.Use(m.Auth(u)) + + route.Get("/", ctrl.GetAll) + route.Post("/", ctrl.CreateOne) + route.Get("/:id", ctrl.GetOne) + route.Patch("/:id", ctrl.UpdateOne) + route.Delete("/:id", ctrl.DeleteOne) +} diff --git a/internal/modules/master/config-checklists/services/config-checklist.service.go b/internal/modules/master/config-checklists/services/config-checklist.service.go new file mode 100644 index 00000000..0c96e3d5 --- /dev/null +++ b/internal/modules/master/config-checklists/services/config-checklist.service.go @@ -0,0 +1,146 @@ +package service + +import ( + "errors" + "time" + + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + repository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/config-checklists/repositories" + validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/config-checklists/validations" + "gitlab.com/mbugroup/lti-api.git/internal/utils" + + "github.com/go-playground/validator/v10" + "github.com/gofiber/fiber/v2" + "github.com/sirupsen/logrus" + "gorm.io/gorm" +) + +type ConfigChecklistService interface { + GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ConfigChecklist, int64, error) + GetOne(ctx *fiber.Ctx, id uint) (*entity.ConfigChecklist, error) + CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.ConfigChecklist, error) + UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.ConfigChecklist, error) + DeleteOne(ctx *fiber.Ctx, id uint) error +} + +type configChecklistService struct { + Log *logrus.Logger + Validate *validator.Validate + Repository repository.ConfigChecklistRepository +} + +func NewConfigChecklistService(repo repository.ConfigChecklistRepository, validate *validator.Validate) ConfigChecklistService { + return &configChecklistService{ + Log: utils.Log, + Validate: validate, + Repository: repo, + } +} + +func (s configChecklistService) withRelations(db *gorm.DB) *gorm.DB { + return db +} + +func (s configChecklistService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ConfigChecklist, int64, error) { + if err := s.Validate.Struct(params); err != nil { + return nil, 0, err + } + + offset := (params.Page - 1) * params.Limit + + configChecklists, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { + db = s.withRelations(db) + return db.Order("date DESC").Order("created_at DESC") + }) + + if err != nil { + s.Log.Errorf("Failed to get configChecklists: %+v", err) + return nil, 0, err + } + return configChecklists, total, nil +} + +func (s configChecklistService) GetOne(c *fiber.Ctx, id uint) (*entity.ConfigChecklist, error) { + configChecklist, err := s.Repository.GetByID(c.Context(), id, s.withRelations) + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusNotFound, "ConfigChecklist not found") + } + if err != nil { + s.Log.Errorf("Failed get configChecklist by id: %+v", err) + return nil, err + } + return configChecklist, nil +} + +func (s *configChecklistService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.ConfigChecklist, error) { + if err := s.Validate.Struct(req); err != nil { + return nil, err + } + + date, err := time.Parse("2006-01-02", req.Date) + if err != nil { + return nil, fiber.NewError(fiber.StatusBadRequest, "invalid date format, use YYYY-MM-DD") + } + + createBody := &entity.ConfigChecklist{ + Date: date, + PercentageThresholdBad: req.PercentageThresholdBad, + PercentageThresholdEnough: req.PercentageThresholdEnough, + } + + if err := s.Repository.CreateOne(c.Context(), createBody, nil); err != nil { + s.Log.Errorf("Failed to create configChecklist: %+v", err) + return nil, err + } + + return s.GetOne(c, createBody.Id) +} + +func (s configChecklistService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.ConfigChecklist, error) { + if err := s.Validate.Struct(req); err != nil { + return nil, err + } + + updateBody := make(map[string]any) + + if req.Date != nil { + date, err := time.Parse("2006-01-02", *req.Date) + if err != nil { + return nil, fiber.NewError(fiber.StatusBadRequest, "invalid date format, use YYYY-MM-DD") + } + updateBody["date"] = date + } + + if req.PercentageThresholdBad != nil { + updateBody["percentage_threshold_bad"] = *req.PercentageThresholdBad + } + + if req.PercentageThresholdEnough != nil { + updateBody["percentage_threshold_enough"] = *req.PercentageThresholdEnough + } + + if len(updateBody) == 0 { + return s.GetOne(c, id) + } + + if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusNotFound, "ConfigChecklist not found") + } + s.Log.Errorf("Failed to update configChecklist: %+v", err) + return nil, err + } + + return s.GetOne(c, id) +} + +func (s configChecklistService) DeleteOne(c *fiber.Ctx, id uint) error { + if err := s.Repository.DeleteOne(c.Context(), id); err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, "ConfigChecklist not found") + } + s.Log.Errorf("Failed to delete configChecklist: %+v", err) + return err + } + return nil +} diff --git a/internal/modules/master/config-checklists/validations/config-checklist.validation.go b/internal/modules/master/config-checklists/validations/config-checklist.validation.go new file mode 100644 index 00000000..10f477b7 --- /dev/null +++ b/internal/modules/master/config-checklists/validations/config-checklist.validation.go @@ -0,0 +1,19 @@ +package validation + +type Create struct { + Date string `json:"date" validate:"required"` + PercentageThresholdBad int `json:"percentage_threshold_bad" validate:"required"` + PercentageThresholdEnough int `json:"percentage_threshold_enough" validate:"required"` +} + +type Update struct { + Date *string `json:"date,omitempty" validate:"omitempty"` + PercentageThresholdBad *int `json:"percentage_threshold_bad,omitempty" validate:"omitempty"` + PercentageThresholdEnough *int `json:"percentage_threshold_enough,omitempty" validate:"omitempty"` +} + +type Query struct { + Page int `query:"page" validate:"omitempty,number,min=1,gt=0"` + Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"` + Search string `query:"search" validate:"omitempty,max=50"` +} diff --git a/internal/modules/master/route.go b/internal/modules/master/route.go index f9bc7b13..06ba1ae3 100644 --- a/internal/modules/master/route.go +++ b/internal/modules/master/route.go @@ -24,6 +24,7 @@ import ( 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" + configChecklists "gitlab.com/mbugroup/lti-api.git/internal/modules/master/config-checklists" // MODULE IMPORTS ) @@ -48,6 +49,7 @@ func RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Valida employeess.EmployeesModule{}, phasess.PhasesModule{}, phaseActivitys.PhaseActivityModule{}, + configChecklists.ConfigChecklistModule{}, // MODULE REGISTRY }