From 39909d1c2e41b570233964bdb3a120e404261c73 Mon Sep 17 00:00:00 2001 From: MacBook Air M1 Date: Fri, 2 Jan 2026 11:24:26 +0700 Subject: [PATCH 01/10] first commit api production-result --- internal/entities/recording_egg.go | 1 + .../controllers/repport.controller.go | 44 ++- .../dto/repportProductionResult.dto.go | 43 +++ internal/modules/repports/module.go | 3 +- .../production_result.repository.go | 79 ++++ internal/modules/repports/route.go | 1 + .../repports/services/repport.service.go | 349 ++++++++++++++++++ .../validations/repport.validation.go | 6 + 8 files changed, 523 insertions(+), 3 deletions(-) create mode 100644 internal/modules/repports/dto/repportProductionResult.dto.go create mode 100644 internal/modules/repports/repositories/production_result.repository.go diff --git a/internal/entities/recording_egg.go b/internal/entities/recording_egg.go index 775d15dc..90546448 100644 --- a/internal/entities/recording_egg.go +++ b/internal/entities/recording_egg.go @@ -12,6 +12,7 @@ type RecordingEgg struct { CreatedAt time.Time `gorm:"autoCreateTime"` UpdatedAt time.Time `gorm:"autoUpdateTime"` ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"` + ProductFlagName *string `gorm:"column:product_flag_name" json:"-"` CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"` Recording Recording `gorm:"foreignKey:RecordingId;references:Id"` } diff --git a/internal/modules/repports/controllers/repport.controller.go b/internal/modules/repports/controllers/repport.controller.go index 82229a45..39136e85 100644 --- a/internal/modules/repports/controllers/repport.controller.go +++ b/internal/modules/repports/controllers/repport.controller.go @@ -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, + }) +} diff --git a/internal/modules/repports/dto/repportProductionResult.dto.go b/internal/modules/repports/dto/repportProductionResult.dto.go new file mode 100644 index 00000000..ab2b3e0c --- /dev/null +++ b/internal/modules/repports/dto/repportProductionResult.dto.go @@ -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"` +} diff --git a/internal/modules/repports/module.go b/internal/modules/repports/module.go index 105d9ad5..40a3c0f3 100644 --- a/internal/modules/repports/module.go +++ b/internal/modules/repports/module.go @@ -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) diff --git a/internal/modules/repports/repositories/production_result.repository.go b/internal/modules/repports/repositories/production_result.repository.go new file mode 100644 index 00000000..f2decedf --- /dev/null +++ b/internal/modules/repports/repositories/production_result.repository.go @@ -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 +} diff --git a/internal/modules/repports/route.go b/internal/modules/repports/route.go index 707ef878..0da9adb2 100644 --- a/internal/modules/repports/route.go +++ b/internal/modules/repports/route.go @@ -19,5 +19,6 @@ func RepportRoutes(v1 fiber.Router, u user.UserService, s repport.RepportService 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", ctrl.GetHppPerKandang) + route.Get("/production-result/:idProjectFlockKandang", ctrl.GetProductionResult) } diff --git a/internal/modules/repports/services/repport.service.go b/internal/modules/repports/services/repport.service.go index e2232a02..d2a5c982 100644 --- a/internal/modules/repports/services/repport.service.go +++ b/internal/modules/repports/services/repport.service.go @@ -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, } } @@ -230,6 +234,351 @@ 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) + 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 diff --git a/internal/modules/repports/validations/repport.validation.go b/internal/modules/repports/validations/repport.validation.go index 47a711cc..b909d77c 100644 --- a/internal/modules/repports/validations/repport.validation.go +++ b/internal/modules/repports/validations/repport.validation.go @@ -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"` +} From df504e3ff03cc351788ce2d571babbc31047f3c7 Mon Sep 17 00:00:00 2001 From: MacBook Air M1 Date: Mon, 5 Jan 2026 17:17:25 +0700 Subject: [PATCH 02/10] add migration;add api create employee --- ...44_create_daily_checklists_tables.down.sql | 12 + ...1644_create_daily_checklists_tables.up.sql | 194 ++++++++++++++++ internal/entities/employee.go | 26 +++ internal/entities/phase.go | 41 ++++ .../controllers/employees.controller.go | 144 ++++++++++++ .../master/employees/dto/employees.dto.go | 70 ++++++ internal/modules/master/employees/module.go | 25 +++ .../repositories/employees.repository.go | 21 ++ internal/modules/master/employees/route.go | 23 ++ .../employees/services/employees.service.go | 209 ++++++++++++++++++ .../validations/employees.validation.go | 19 ++ internal/modules/master/route.go | 4 +- 12 files changed, 787 insertions(+), 1 deletion(-) create mode 100644 internal/database/migrations/20260105131644_create_daily_checklists_tables.down.sql create mode 100644 internal/database/migrations/20260105131644_create_daily_checklists_tables.up.sql create mode 100644 internal/entities/employee.go create mode 100644 internal/entities/phase.go create mode 100644 internal/modules/master/employees/controllers/employees.controller.go create mode 100644 internal/modules/master/employees/dto/employees.dto.go create mode 100644 internal/modules/master/employees/module.go create mode 100644 internal/modules/master/employees/repositories/employees.repository.go create mode 100644 internal/modules/master/employees/route.go create mode 100644 internal/modules/master/employees/services/employees.service.go create mode 100644 internal/modules/master/employees/validations/employees.validation.go diff --git a/internal/database/migrations/20260105131644_create_daily_checklists_tables.down.sql b/internal/database/migrations/20260105131644_create_daily_checklists_tables.down.sql new file mode 100644 index 00000000..7be30be1 --- /dev/null +++ b/internal/database/migrations/20260105131644_create_daily_checklists_tables.down.sql @@ -0,0 +1,12 @@ +DROP TABLE IF EXISTS daily_checklist_tasks; +DROP TABLE IF EXISTS daily_checklist_activity_task_assignees; +DROP TABLE IF EXISTS daily_checklist_activity_tasks; +DROP TABLE IF EXISTS daily_checklist_phases; +DROP TABLE IF EXISTS daily_checklists; +DROP TABLE IF EXISTS checklists; +DROP TABLE IF EXISTS phase_activities; +DROP TABLE IF EXISTS phases; +DROP TABLE IF EXISTS employee_kandangs; +DROP TABLE IF EXISTS employees; + +DROP TYPE IF EXISTS category_code; diff --git a/internal/database/migrations/20260105131644_create_daily_checklists_tables.up.sql b/internal/database/migrations/20260105131644_create_daily_checklists_tables.up.sql new file mode 100644 index 00000000..6074fa8c --- /dev/null +++ b/internal/database/migrations/20260105131644_create_daily_checklists_tables.up.sql @@ -0,0 +1,194 @@ +CREATE TYPE category_code AS ENUM ( + 'pullet_open', + 'pullet_close', + 'produksi_open', + 'produksi_close' +); + +-- MASTER TABLES + +CREATE TABLE employees ( + id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + name varchar NOT NULL, + is_active boolean NOT NULL DEFAULT true, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now() +); + +CREATE TABLE employee_kandangs ( + id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + employee_id bigint NOT NULL, + kandang_id bigint NOT NULL, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + + CONSTRAINT fk_employee_kandangs_employee + FOREIGN KEY (employee_id) REFERENCES employees(id) + ON DELETE CASCADE, + + CONSTRAINT fk_employee_kandangs_kandang + FOREIGN KEY (kandang_id) REFERENCES kandangs(id) + ON DELETE CASCADE, + + CONSTRAINT uq_employee_kandangs UNIQUE (employee_id, kandang_id) +); + +-- PHASE & CHECKLIST + +CREATE TABLE phases ( + id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + name varchar NOT NULL, + is_active boolean NOT NULL DEFAULT true, + category category_code NOT NULL, + created_at timestamptz NOT NULL DEFAULT now() +); + +CREATE TABLE phase_activities ( + id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + phase_id bigint NOT NULL, + name varchar NOT NULL, + description text, + time_type text, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + + CONSTRAINT fk_phase_activities_phase + FOREIGN KEY (phase_id) REFERENCES phases(id) + ON DELETE CASCADE +); + +CREATE TABLE checklists ( + id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + name varchar NOT NULL, + description text, + phase_id bigint, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + deleted_at timestamptz, + + CONSTRAINT fk_checklists_phase + FOREIGN KEY (phase_id) REFERENCES phases(id) + ON DELETE SET NULL +); + + +-- DAILY CHECKLISTS +CREATE TABLE daily_checklists ( + id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + kandang_id bigint NOT NULL, + checklist_id bigint NOT NULL, + date date NOT NULL, + name varchar, + status varchar, + category category_code NOT NULL, + total_score integer, + document_path varchar, + reject_reason text, + created_by bigint, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + + CONSTRAINT fk_daily_checklists_kandang + FOREIGN KEY (kandang_id) REFERENCES kandangs(id) + ON DELETE CASCADE, + + CONSTRAINT fk_daily_checklists_checklist + FOREIGN KEY (checklist_id) REFERENCES checklists(id) + ON DELETE RESTRICT, + + CONSTRAINT fk_daily_checklists_created_by + FOREIGN KEY (created_by) REFERENCES users(id) + ON DELETE SET NULL +); + + +--RELASI CHECKLIST ⇄ PHASE + +CREATE TABLE daily_checklist_phases ( + id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + checklist_id bigint NOT NULL, + phase_id bigint NOT NULL, + created_at timestamptz NOT NULL DEFAULT now(), + + CONSTRAINT fk_dcp_checklist + FOREIGN KEY (checklist_id) REFERENCES checklists(id) + ON DELETE CASCADE, + + CONSTRAINT fk_dcp_phase + FOREIGN KEY (phase_id) REFERENCES phases(id) + ON DELETE CASCADE, + + CONSTRAINT uq_daily_checklist_phases UNIQUE (checklist_id, phase_id) +); + + +--ACTIVITY TASKS & ASSIGNMENT + + +CREATE TABLE daily_checklist_activity_tasks ( + id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + checklist_id bigint NOT NULL, + phase_id bigint NOT NULL, + phase_activity_id bigint NOT NULL, + time_type text, + notes text, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + + CONSTRAINT fk_dcat_checklist + FOREIGN KEY (checklist_id) REFERENCES checklists(id) + ON DELETE CASCADE, + + CONSTRAINT fk_dcat_phase + FOREIGN KEY (phase_id) REFERENCES phases(id) + ON DELETE CASCADE, + + CONSTRAINT fk_dcat_phase_activity + FOREIGN KEY (phase_activity_id) REFERENCES phase_activities(id) + ON DELETE CASCADE +); + +CREATE TABLE daily_checklist_activity_task_assignments ( + id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + task_id bigint NOT NULL, + employee_id bigint NOT NULL, + checked boolean NOT NULL DEFAULT false, + note text, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + + CONSTRAINT fk_assignment_task + FOREIGN KEY (task_id) REFERENCES daily_checklist_activity_tasks(id) + ON DELETE CASCADE, + + CONSTRAINT fk_assignment_employee + FOREIGN KEY (employee_id) REFERENCES employees(id) + ON DELETE CASCADE +); + +--DAILY CHECKLIST TASK RESULT +CREATE TABLE daily_checklist_tasks ( + id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + daily_checklist_id bigint NOT NULL, + checklist_id bigint NOT NULL, + checklist_item_id bigint, + is_completed boolean NOT NULL DEFAULT false, + score_value integer, + notes text, + photo_proof varchar, + status varchar, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + + CONSTRAINT fk_dct_daily + FOREIGN KEY (daily_checklist_id) REFERENCES daily_checklists(id) + ON DELETE CASCADE, + + CONSTRAINT fk_dct_checklist + FOREIGN KEY (checklist_id) REFERENCES checklists(id) + ON DELETE CASCADE, + + CONSTRAINT fk_dct_checklist_item + FOREIGN KEY (checklist_item_id) REFERENCES phase_activities(id) + ON DELETE SET NULL +); diff --git a/internal/entities/employee.go b/internal/entities/employee.go new file mode 100644 index 00000000..5810c6ee --- /dev/null +++ b/internal/entities/employee.go @@ -0,0 +1,26 @@ +package entities + +import "time" + +type Employee struct { + Id uint `gorm:"primaryKey"` + Name string `gorm:"not null"` + IsActive bool `gorm:"not null"` + CreatedAt time.Time `gorm:"autoCreateTime"` + UpdatedAt time.Time `gorm:"autoUpdateTime"` + + EmployeeKandangs []EmployeeKandang `gorm:"foreignKey:EmployeeId;references:Id"` +} + +type Employees = Employee + +type EmployeeKandang struct { + Id uint `gorm:"primaryKey"` + EmployeeId uint `gorm:"not null"` + KandangId uint `gorm:"not null"` + CreatedAt time.Time `gorm:"autoCreateTime"` + UpdatedAt time.Time `gorm:"autoUpdateTime"` + + Employee Employee `gorm:"foreignKey:EmployeeId;references:Id"` + Kandang Kandang `gorm:"foreignKey:KandangId;references:Id"` +} diff --git a/internal/entities/phase.go b/internal/entities/phase.go new file mode 100644 index 00000000..4ee80804 --- /dev/null +++ b/internal/entities/phase.go @@ -0,0 +1,41 @@ +package entities + +import ( + "time" + + "gorm.io/gorm" +) + +type Phase struct { + Id uint `gorm:"primaryKey"` + Name string `gorm:"not null"` + IsActive bool `gorm:"not null;default:true"` + Category string `gorm:"type:category_code;not null"` + CreatedAt time.Time `gorm:"autoCreateTime"` + + Activities []PhaseActivity `gorm:"foreignKey:PhaseId;references:Id"` +} + +type PhaseActivity struct { + Id uint `gorm:"primaryKey"` + PhaseId uint `gorm:"not null"` + Name string `gorm:"not null"` + Description *string `gorm:"type:text"` + TimeType *string `gorm:"type:text"` + CreatedAt time.Time `gorm:"autoCreateTime"` + UpdatedAt time.Time `gorm:"autoUpdateTime"` + + Phase Phase `gorm:"foreignKey:PhaseId;references:Id"` +} + +type Checklist struct { + Id uint `gorm:"primaryKey"` + Name string `gorm:"not null"` + Description *string `gorm:"type:text"` + PhaseId *uint + CreatedAt time.Time `gorm:"autoCreateTime"` + UpdatedAt time.Time `gorm:"autoUpdateTime"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` + + Phase *Phase `gorm:"foreignKey:PhaseId;references:Id"` +} diff --git a/internal/modules/master/employees/controllers/employees.controller.go b/internal/modules/master/employees/controllers/employees.controller.go new file mode 100644 index 00000000..6be28200 --- /dev/null +++ b/internal/modules/master/employees/controllers/employees.controller.go @@ -0,0 +1,144 @@ +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") + } + + 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", + }) +} diff --git a/internal/modules/master/employees/dto/employees.dto.go b/internal/modules/master/employees/dto/employees.dto.go new file mode 100644 index 00000000..65b1b5ca --- /dev/null +++ b/internal/modules/master/employees/dto/employees.dto.go @@ -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), + } +} diff --git a/internal/modules/master/employees/module.go b/internal/modules/master/employees/module.go new file mode 100644 index 00000000..a916ced6 --- /dev/null +++ b/internal/modules/master/employees/module.go @@ -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) +} diff --git a/internal/modules/master/employees/repositories/employees.repository.go b/internal/modules/master/employees/repositories/employees.repository.go new file mode 100644 index 00000000..f10a5884 --- /dev/null +++ b/internal/modules/master/employees/repositories/employees.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 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), + } +} diff --git a/internal/modules/master/employees/route.go b/internal/modules/master/employees/route.go new file mode 100644 index 00000000..53974814 --- /dev/null +++ b/internal/modules/master/employees/route.go @@ -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) +} diff --git a/internal/modules/master/employees/services/employees.service.go b/internal/modules/master/employees/services/employees.service.go new file mode 100644 index 00000000..c17f941a --- /dev/null +++ b/internal/modules/master/employees/services/employees.service.go @@ -0,0 +1,209 @@ +package service + +import ( + "errors" + "fmt" + "strconv" + "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"). + Preload("EmployeeKandangs.Kandang.Location"). + Preload("EmployeeKandangs.Kandang.Pic"). + Preload("EmployeeKandangs.Kandang.CreatedUser") +} + +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 != "" { + 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 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, err := parseKandangIDs(req.KandangIDs) + if err != nil { + return nil, fiber.NewError(fiber.StatusBadRequest, err.Error()) + } + + 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) + + 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, "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 parseKandangIDs(raw string) ([]uint, error) { + parts := strings.Split(raw, ",") + ids := make([]uint, 0, len(parts)) + seen := make(map[uint]struct{}) + + for _, part := range parts { + value := strings.TrimSpace(part) + if value == "" { + continue + } + + parsed, err := strconv.ParseUint(value, 10, 64) + if err != nil { + return nil, fmt.Errorf("invalid kandang id: %s", value) + } + + id := uint(parsed) + if _, ok := seen[id]; ok { + continue + } + seen[id] = struct{}{} + ids = append(ids, id) + } + + if len(ids) == 0 { + return nil, errors.New("kandang_ids must contain at least one valid id") + } + + return ids, nil +} diff --git a/internal/modules/master/employees/validations/employees.validation.go b/internal/modules/master/employees/validations/employees.validation.go new file mode 100644 index 00000000..4449bfcc --- /dev/null +++ b/internal/modules/master/employees/validations/employees.validation.go @@ -0,0 +1,19 @@ +package validation + +type Create struct { + Name string `json:"name" validate:"required_strict,min=3"` + KandangIDs string `json:"kandang_ids" validate:"required"` + IsActive bool `json:"is_active"` +} + +type Update struct { + Name *string `json:"name,omitempty" validate:"omitempty"` + KandangIDs *string `json:"kandang_ids,omitempty"` + 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"` +} diff --git a/internal/modules/master/route.go b/internal/modules/master/route.go index 26ae28ee..2965baae 100644 --- a/internal/modules/master/route.go +++ b/internal/modules/master/route.go @@ -10,17 +10,18 @@ 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" 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 +43,7 @@ func RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Valida banks.BankModule{}, flocks.FlockModule{}, productionStandards.ProductionStandardModule{}, + employeess.EmployeesModule{}, // MODULE REGISTRY } From 80109b77db6cde912ecf1312511b882accba0a11 Mon Sep 17 00:00:00 2001 From: MacBook Air M1 Date: Mon, 5 Jan 2026 17:32:41 +0700 Subject: [PATCH 03/10] adjust api get all employees --- .../controllers/employees.controller.go | 19 ++++++++++++++++++- .../employees/services/employees.service.go | 19 +++++++++++++------ .../validations/employees.validation.go | 8 +++++--- 3 files changed, 36 insertions(+), 10 deletions(-) diff --git a/internal/modules/master/employees/controllers/employees.controller.go b/internal/modules/master/employees/controllers/employees.controller.go index 6be28200..3d0901c8 100644 --- a/internal/modules/master/employees/controllers/employees.controller.go +++ b/internal/modules/master/employees/controllers/employees.controller.go @@ -29,10 +29,27 @@ func (u *EmployeesController) GetAll(c *fiber.Ctx) error { Search: c.Query("search", ""), } - if query.Page < 1 || query.Limit < 1 { + if query.Page < 1 || query.Limit < 1 { return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0") } + 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 diff --git a/internal/modules/master/employees/services/employees.service.go b/internal/modules/master/employees/services/employees.service.go index c17f941a..df131c23 100644 --- a/internal/modules/master/employees/services/employees.service.go +++ b/internal/modules/master/employees/services/employees.service.go @@ -41,10 +41,10 @@ func NewEmployeesService(repo repository.EmployeesRepository, validate *validato func (s employeesService) withRelations(db *gorm.DB) *gorm.DB { return db. - Preload("EmployeeKandangs.Kandang"). - Preload("EmployeeKandangs.Kandang.Location"). - Preload("EmployeeKandangs.Kandang.Pic"). - Preload("EmployeeKandangs.Kandang.CreatedUser") + Preload("EmployeeKandangs.Kandang") + // Preload("EmployeeKandangs.Kandang.Location"). + // Preload("EmployeeKandangs.Kandang.Pic"). + // Preload("EmployeeKandangs.Kandang.CreatedUser") } func (s employeesService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Employees, int64, error) { @@ -57,9 +57,16 @@ func (s employeesService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti employeess, 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+"%") + db = db.Where("employees.name LIKE ?", "%"+params.Search+"%") } - return db.Order("created_at DESC").Order("updated_at DESC") + 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 { diff --git a/internal/modules/master/employees/validations/employees.validation.go b/internal/modules/master/employees/validations/employees.validation.go index 4449bfcc..159b875f 100644 --- a/internal/modules/master/employees/validations/employees.validation.go +++ b/internal/modules/master/employees/validations/employees.validation.go @@ -13,7 +13,9 @@ type Update struct { } 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"` + 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"` } From 9f840f265029236d43e937ff8fa5daec6471bac3 Mon Sep 17 00:00:00 2001 From: MacBook Air M1 Date: Mon, 5 Jan 2026 17:49:44 +0700 Subject: [PATCH 04/10] adjust patch employee --- .../employees/services/employees.service.go | 70 ++++++++++++++++++- 1 file changed, 67 insertions(+), 3 deletions(-) diff --git a/internal/modules/master/employees/services/employees.service.go b/internal/modules/master/employees/services/employees.service.go index df131c23..aa82255d 100644 --- a/internal/modules/master/employees/services/employees.service.go +++ b/internal/modules/master/employees/services/employees.service.go @@ -153,16 +153,80 @@ func (s employeesService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin } updateBody := make(map[string]any) + var ( + kandangIDs []uint + needKandangUpdate bool + ) if req.Name != nil { - updateBody["name"] = *req.Name + 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 len(updateBody) == 0 { + if req.IsActive != nil { + updateBody["is_active"] = *req.IsActive + } + + if req.KandangIDs != nil { + ids, err := parseKandangIDs(*req.KandangIDs) + if err != nil { + return nil, fiber.NewError(fiber.StatusBadRequest, err.Error()) + } + + kandangIDs = ids + needKandangUpdate = true + } + + if len(updateBody) == 0 && !needKandangUpdate { return s.GetOne(c, id) } - if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil { + 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") } From 4a08be1f55d05500121417f5b375fad1808a9d05 Mon Sep 17 00:00:00 2001 From: MacBook Air M1 Date: Mon, 5 Jan 2026 19:59:03 +0700 Subject: [PATCH 05/10] add module master data phases --- internal/entities/phase.go | 2 +- .../phasess/controllers/phases.controller.go | 148 +++++++++++++++++ .../modules/master/phasess/dto/phases.dto.go | 68 ++++++++ internal/modules/master/phasess/module.go | 25 +++ .../phasess/repositories/phases.repository.go | 21 +++ internal/modules/master/phasess/route.go | 23 +++ .../master/phasess/services/phases.service.go | 152 ++++++++++++++++++ .../phasess/validations/phases.validation.go | 17 ++ internal/modules/master/route.go | 2 + 9 files changed, 457 insertions(+), 1 deletion(-) create mode 100644 internal/modules/master/phasess/controllers/phases.controller.go create mode 100644 internal/modules/master/phasess/dto/phases.dto.go create mode 100644 internal/modules/master/phasess/module.go create mode 100644 internal/modules/master/phasess/repositories/phases.repository.go create mode 100644 internal/modules/master/phasess/route.go create mode 100644 internal/modules/master/phasess/services/phases.service.go create mode 100644 internal/modules/master/phasess/validations/phases.validation.go diff --git a/internal/entities/phase.go b/internal/entities/phase.go index 4ee80804..d30369eb 100644 --- a/internal/entities/phase.go +++ b/internal/entities/phase.go @@ -6,7 +6,7 @@ import ( "gorm.io/gorm" ) -type Phase struct { +type Phases struct { Id uint `gorm:"primaryKey"` Name string `gorm:"not null"` IsActive bool `gorm:"not null;default:true"` diff --git a/internal/modules/master/phasess/controllers/phases.controller.go b/internal/modules/master/phasess/controllers/phases.controller.go new file mode 100644 index 00000000..c9d9d349 --- /dev/null +++ b/internal/modules/master/phasess/controllers/phases.controller.go @@ -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", + }) +} diff --git a/internal/modules/master/phasess/dto/phases.dto.go b/internal/modules/master/phasess/dto/phases.dto.go new file mode 100644 index 00000000..51724556 --- /dev/null +++ b/internal/modules/master/phasess/dto/phases.dto.go @@ -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), + } +} diff --git a/internal/modules/master/phasess/module.go b/internal/modules/master/phasess/module.go new file mode 100644 index 00000000..3f44c220 --- /dev/null +++ b/internal/modules/master/phasess/module.go @@ -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) +} diff --git a/internal/modules/master/phasess/repositories/phases.repository.go b/internal/modules/master/phasess/repositories/phases.repository.go new file mode 100644 index 00000000..d243ca2e --- /dev/null +++ b/internal/modules/master/phasess/repositories/phases.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 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), + } +} diff --git a/internal/modules/master/phasess/route.go b/internal/modules/master/phasess/route.go new file mode 100644 index 00000000..b4ca202d --- /dev/null +++ b/internal/modules/master/phasess/route.go @@ -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) +} diff --git a/internal/modules/master/phasess/services/phases.service.go b/internal/modules/master/phasess/services/phases.service.go new file mode 100644 index 00000000..863b369d --- /dev/null +++ b/internal/modules/master/phasess/services/phases.service.go @@ -0,0 +1,152 @@ +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) = ?", strings.ToLower(req.Name)) + }); 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 + } + + 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 id <> ?", strings.ToLower(*req.Name), 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 { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusNotFound, "Phases not found") + } + 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 +} diff --git a/internal/modules/master/phasess/validations/phases.validation.go b/internal/modules/master/phasess/validations/phases.validation.go new file mode 100644 index 00000000..c22d4208 --- /dev/null +++ b/internal/modules/master/phasess/validations/phases.validation.go @@ -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"` +} diff --git a/internal/modules/master/route.go b/internal/modules/master/route.go index 2965baae..e0a7b246 100644 --- a/internal/modules/master/route.go +++ b/internal/modules/master/route.go @@ -22,6 +22,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" + phasess "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phasess" // MODULE IMPORTS ) @@ -44,6 +45,7 @@ func RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Valida flocks.FlockModule{}, productionStandards.ProductionStandardModule{}, employeess.EmployeesModule{}, + phasess.PhasesModule{}, // MODULE REGISTRY } From b1996be24c114708062dd10eb2a1b716dd889d17 Mon Sep 17 00:00:00 2001 From: MacBook Air M1 Date: Mon, 5 Jan 2026 22:25:46 +0700 Subject: [PATCH 06/10] add module master phase activity --- .../controllers/phase-activity.controller.go | 153 ++++++++++++++++ .../dto/phase-activity.dto.go | 72 ++++++++ .../modules/master/phase-activities/module.go | 27 +++ .../repositories/phase-activity.repository.go | 21 +++ .../modules/master/phase-activities/route.go | 23 +++ .../services/phase-activity.service.go | 167 ++++++++++++++++++ .../validations/phase-activity.validation.go | 21 +++ .../master/phasess/services/phases.service.go | 16 +- internal/modules/master/route.go | 4 +- 9 files changed, 498 insertions(+), 6 deletions(-) create mode 100644 internal/modules/master/phase-activities/controllers/phase-activity.controller.go create mode 100644 internal/modules/master/phase-activities/dto/phase-activity.dto.go create mode 100644 internal/modules/master/phase-activities/module.go create mode 100644 internal/modules/master/phase-activities/repositories/phase-activity.repository.go create mode 100644 internal/modules/master/phase-activities/route.go create mode 100644 internal/modules/master/phase-activities/services/phase-activity.service.go create mode 100644 internal/modules/master/phase-activities/validations/phase-activity.validation.go diff --git a/internal/modules/master/phase-activities/controllers/phase-activity.controller.go b/internal/modules/master/phase-activities/controllers/phase-activity.controller.go new file mode 100644 index 00000000..455ff1e4 --- /dev/null +++ b/internal/modules/master/phase-activities/controllers/phase-activity.controller.go @@ -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", + }) +} diff --git a/internal/modules/master/phase-activities/dto/phase-activity.dto.go b/internal/modules/master/phase-activities/dto/phase-activity.dto.go new file mode 100644 index 00000000..ee5942d5 --- /dev/null +++ b/internal/modules/master/phase-activities/dto/phase-activity.dto.go @@ -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), + } +} diff --git a/internal/modules/master/phase-activities/module.go b/internal/modules/master/phase-activities/module.go new file mode 100644 index 00000000..22d25189 --- /dev/null +++ b/internal/modules/master/phase-activities/module.go @@ -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) +} diff --git a/internal/modules/master/phase-activities/repositories/phase-activity.repository.go b/internal/modules/master/phase-activities/repositories/phase-activity.repository.go new file mode 100644 index 00000000..cc5eaae5 --- /dev/null +++ b/internal/modules/master/phase-activities/repositories/phase-activity.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 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), + } +} diff --git a/internal/modules/master/phase-activities/route.go b/internal/modules/master/phase-activities/route.go new file mode 100644 index 00000000..6fcef558 --- /dev/null +++ b/internal/modules/master/phase-activities/route.go @@ -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) +} diff --git a/internal/modules/master/phase-activities/services/phase-activity.service.go b/internal/modules/master/phase-activities/services/phase-activity.service.go new file mode 100644 index 00000000..3426eab4 --- /dev/null +++ b/internal/modules/master/phase-activities/services/phase-activity.service.go @@ -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 +} diff --git a/internal/modules/master/phase-activities/validations/phase-activity.validation.go b/internal/modules/master/phase-activities/validations/phase-activity.validation.go new file mode 100644 index 00000000..a2ab8e1b --- /dev/null +++ b/internal/modules/master/phase-activities/validations/phase-activity.validation.go @@ -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"` +} diff --git a/internal/modules/master/phasess/services/phases.service.go b/internal/modules/master/phasess/services/phases.service.go index 863b369d..98e73bef 100644 --- a/internal/modules/master/phasess/services/phases.service.go +++ b/internal/modules/master/phasess/services/phases.service.go @@ -84,7 +84,7 @@ func (s *phasesService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity } if _, err := s.Repository.First(c.Context(), func(db *gorm.DB) *gorm.DB { - return db.Where("LOWER(name) = ?", strings.ToLower(req.Name)) + 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) { @@ -111,11 +111,20 @@ func (s phasesService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) 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 id <> ?", strings.ToLower(*req.Name), id) + 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) { @@ -130,9 +139,6 @@ func (s phasesService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) } if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, fiber.NewError(fiber.StatusNotFound, "Phases not found") - } s.Log.Errorf("Failed to update phases: %+v", err) return nil, err } diff --git a/internal/modules/master/route.go b/internal/modules/master/route.go index e0a7b246..f9bc7b13 100644 --- a/internal/modules/master/route.go +++ b/internal/modules/master/route.go @@ -16,13 +16,14 @@ import ( 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" - phasess "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phasess" // MODULE IMPORTS ) @@ -46,6 +47,7 @@ func RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Valida productionStandards.ProductionStandardModule{}, employeess.EmployeesModule{}, phasess.PhasesModule{}, + phaseActivitys.PhaseActivityModule{}, // MODULE REGISTRY } From 1bdaf63763d99c4fa16404f511a349e8647492d4 Mon Sep 17 00:00:00 2001 From: ragilap Date: Tue, 6 Jan 2026 12:02:19 +0700 Subject: [PATCH 07/10] feat(BE-281): adjustment sso redirect,adjustment response closing,adjustment uniformity --- internal/config/config.go | 2 + .../closings/dto/closingSapronak.dto.go | 19 +- .../closings/services/sapronak.service.go | 6 +- .../services/projectflock.service.go | 21 -- .../controllers/uniformity.controller.go | 6 +- .../uniformities/dto/uniformity.dto.go | 18 ++ .../services/uniformity.service.go | 26 ++- .../modules/sso/controllers/sso.controller.go | 203 +++++++++++++++++- 8 files changed, 267 insertions(+), 34 deletions(-) diff --git a/internal/config/config.go b/internal/config/config.go index 5f76a9e0..8660704b 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -54,6 +54,7 @@ var ( SSOAuthorizeURL string SSOTokenURL string SSOGetMeURL string + SSOPortalURL string SSOClients map[string]SSOClientConfig SSOAccessCookieName string SSORefreshCookieName string @@ -131,6 +132,7 @@ func init() { SSOAuthorizeURL = viper.GetString("SSO_AUTHORIZE_URL") SSOTokenURL = viper.GetString("SSO_TOKEN_URL") SSOGetMeURL = viper.GetString("SSO_GETME_URL") + SSOPortalURL = strings.TrimSpace(viper.GetString("SSO_PORTAL_URL")) SSOAccessCookieName = defaultString(viper.GetString("SSO_ACCESS_COOKIE_NAME"), "sso_access") SSORefreshCookieName = defaultString(viper.GetString("SSO_REFRESH_COOKIE_NAME"), "sso_refresh") SSOCookieDomain = viper.GetString("SSO_COOKIE_DOMAIN") diff --git a/internal/modules/closings/dto/closingSapronak.dto.go b/internal/modules/closings/dto/closingSapronak.dto.go index 13044efd..768c727e 100644 --- a/internal/modules/closings/dto/closingSapronak.dto.go +++ b/internal/modules/closings/dto/closingSapronak.dto.go @@ -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 } diff --git a/internal/modules/closings/services/sapronak.service.go b/internal/modules/closings/services/sapronak.service.go index 3c1843dd..b923db5d 100644 --- a/internal/modules/closings/services/sapronak.service.go +++ b/internal/modules/closings/services/sapronak.service.go @@ -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 diff --git a/internal/modules/production/project_flocks/services/projectflock.service.go b/internal/modules/production/project_flocks/services/projectflock.service.go index ec887eea..5f643dee 100644 --- a/internal/modules/production/project_flocks/services/projectflock.service.go +++ b/internal/modules/production/project_flocks/services/projectflock.service.go @@ -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 diff --git a/internal/modules/production/uniformities/controllers/uniformity.controller.go b/internal/modules/production/uniformities/controllers/uniformity.controller.go index ce91c3af..e18e7dce 100644 --- a/internal/modules/production/uniformities/controllers/uniformity.controller.go +++ b/internal/modules/production/uniformities/controllers/uniformity.controller.go @@ -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), }) } diff --git a/internal/modules/production/uniformities/dto/uniformity.dto.go b/internal/modules/production/uniformities/dto/uniformity.dto.go index 0c38d81b..af401a54 100644 --- a/internal/modules/production/uniformities/dto/uniformity.dto.go +++ b/internal/modules/production/uniformities/dto/uniformity.dto.go @@ -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 } diff --git a/internal/modules/production/uniformities/services/uniformity.service.go b/internal/modules/production/uniformities/services/uniformity.service.go index fb7ed9ed..747eb965 100644 --- a/internal/modules/production/uniformities/services/uniformity.service.go +++ b/internal/modules/production/uniformities/services/uniformity.service.go @@ -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 diff --git a/internal/modules/sso/controllers/sso.controller.go b/internal/modules/sso/controllers/sso.controller.go index 554b3388..410e9577 100644 --- a/internal/modules/sso/controllers/sso.controller.go +++ b/internal/modules/sso/controllers/sso.controller.go @@ -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 From 7a26ca5fe5567eef9eab7785343878fd3a735ee7 Mon Sep 17 00:00:00 2001 From: ragilap Date: Tue, 6 Jan 2026 17:01:09 +0700 Subject: [PATCH 08/10] feat(BE-281): adjustment recording to cascade --- ...260106090725_fk_recording_cascade.down.sql | 21 +++++++++++++++++++ ...20260106090725_fk_recording_cascade.up.sql | 21 +++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 internal/database/migrations/20260106090725_fk_recording_cascade.down.sql create mode 100644 internal/database/migrations/20260106090725_fk_recording_cascade.up.sql diff --git a/internal/database/migrations/20260106090725_fk_recording_cascade.down.sql b/internal/database/migrations/20260106090725_fk_recording_cascade.down.sql new file mode 100644 index 00000000..efe3954a --- /dev/null +++ b/internal/database/migrations/20260106090725_fk_recording_cascade.down.sql @@ -0,0 +1,21 @@ +BEGIN; + +DO $$ +BEGIN + IF EXISTS ( + SELECT 1 + FROM pg_constraint + WHERE conname = 'fk_recordings_project_flock_kandang' + ) THEN + ALTER TABLE recordings + DROP CONSTRAINT fk_recordings_project_flock_kandang; + END IF; +END $$; + +ALTER TABLE recordings + ADD CONSTRAINT fk_recordings_project_flock_kandang + FOREIGN KEY (project_flock_kandangs_id) + REFERENCES project_flock_kandangs (id) + ON DELETE RESTRICT ON UPDATE CASCADE; + +COMMIT; diff --git a/internal/database/migrations/20260106090725_fk_recording_cascade.up.sql b/internal/database/migrations/20260106090725_fk_recording_cascade.up.sql new file mode 100644 index 00000000..2600827d --- /dev/null +++ b/internal/database/migrations/20260106090725_fk_recording_cascade.up.sql @@ -0,0 +1,21 @@ +BEGIN; + +DO $$ +BEGIN + IF EXISTS ( + SELECT 1 + FROM pg_constraint + WHERE conname = 'fk_recordings_project_flock_kandang' + ) THEN + ALTER TABLE recordings + DROP CONSTRAINT fk_recordings_project_flock_kandang; + END IF; +END $$; + +ALTER TABLE recordings + ADD CONSTRAINT fk_recordings_project_flock_kandang + FOREIGN KEY (project_flock_kandangs_id) + REFERENCES project_flock_kandangs (id) + ON DELETE CASCADE ON UPDATE CASCADE; + +COMMIT; From 3bd0602525873b644355c5f1b13893e85e0505a2 Mon Sep 17 00:00:00 2001 From: MacBook Air M1 Date: Tue, 6 Jan 2026 17:03:55 +0700 Subject: [PATCH 09/10] add daily checklist module;adjust master data;adjust migration --- ...01434_add_unique_daily_checklists.down.sql | 2 + ...6101434_add_unique_daily_checklists.up.sql | 3 + ..._checklists_checklist_id_nullable.down.sql | 2 + ...ly_checklists_checklist_id_nullable.up.sql | 2 + ..._update_daily_checklist_phases_fk.down.sql | 4 + ...17_update_daily_checklist_phases_fk.up.sql | 4 + ..._daily_checklist_activity_task_fk.down.sql | 4 + ...te_daily_checklist_activity_task_fk.up.sql | 4 + ..._unique_activity_task_assignments.down.sql | 2 + ...dd_unique_activity_task_assignments.up.sql | 3 + ...0_add_deleted_at_to_master_tables.down.sql | 8 + ...640_add_deleted_at_to_master_tables.up.sql | 8 + internal/entities/daily-checklist.go | 81 ++++ internal/entities/employee.go | 17 +- internal/entities/phase.go | 30 +- .../controllers/daily-checklist.controller.go | 243 +++++++++++ .../dto/daily-checklist.dto.go | 76 ++++ internal/modules/daily-checklists/module.go | 27 ++ .../daily-checklist.repository.go | 21 + internal/modules/daily-checklists/route.go | 35 ++ .../services/daily-checklist.service.go | 410 ++++++++++++++++++ .../validations/daily-checklist.validation.go | 26 ++ .../employees/services/employees.service.go | 45 +- .../validations/employees.validation.go | 4 +- internal/route/route.go | 2 + 25 files changed, 1011 insertions(+), 52 deletions(-) create mode 100644 internal/database/migrations/20260106101434_add_unique_daily_checklists.down.sql create mode 100644 internal/database/migrations/20260106101434_add_unique_daily_checklists.up.sql create mode 100644 internal/database/migrations/20260106102657_alter_daily_checklists_checklist_id_nullable.down.sql create mode 100644 internal/database/migrations/20260106102657_alter_daily_checklists_checklist_id_nullable.up.sql create mode 100644 internal/database/migrations/20260106111217_update_daily_checklist_phases_fk.down.sql create mode 100644 internal/database/migrations/20260106111217_update_daily_checklist_phases_fk.up.sql create mode 100644 internal/database/migrations/20260106113936_update_daily_checklist_activity_task_fk.down.sql create mode 100644 internal/database/migrations/20260106113936_update_daily_checklist_activity_task_fk.up.sql create mode 100644 internal/database/migrations/20260106150814_add_unique_activity_task_assignments.down.sql create mode 100644 internal/database/migrations/20260106150814_add_unique_activity_task_assignments.up.sql create mode 100644 internal/database/migrations/20260106164640_add_deleted_at_to_master_tables.down.sql create mode 100644 internal/database/migrations/20260106164640_add_deleted_at_to_master_tables.up.sql create mode 100644 internal/entities/daily-checklist.go create mode 100644 internal/modules/daily-checklists/controllers/daily-checklist.controller.go create mode 100644 internal/modules/daily-checklists/dto/daily-checklist.dto.go create mode 100644 internal/modules/daily-checklists/module.go create mode 100644 internal/modules/daily-checklists/repositories/daily-checklist.repository.go create mode 100644 internal/modules/daily-checklists/route.go create mode 100644 internal/modules/daily-checklists/services/daily-checklist.service.go create mode 100644 internal/modules/daily-checklists/validations/daily-checklist.validation.go diff --git a/internal/database/migrations/20260106101434_add_unique_daily_checklists.down.sql b/internal/database/migrations/20260106101434_add_unique_daily_checklists.down.sql new file mode 100644 index 00000000..f33ea629 --- /dev/null +++ b/internal/database/migrations/20260106101434_add_unique_daily_checklists.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE daily_checklists + DROP CONSTRAINT IF EXISTS daily_checklists_date_kandang_category_key; diff --git a/internal/database/migrations/20260106101434_add_unique_daily_checklists.up.sql b/internal/database/migrations/20260106101434_add_unique_daily_checklists.up.sql new file mode 100644 index 00000000..6566083b --- /dev/null +++ b/internal/database/migrations/20260106101434_add_unique_daily_checklists.up.sql @@ -0,0 +1,3 @@ +ALTER TABLE daily_checklists + ADD CONSTRAINT daily_checklists_date_kandang_category_key + UNIQUE (date, kandang_id, category); diff --git a/internal/database/migrations/20260106102657_alter_daily_checklists_checklist_id_nullable.down.sql b/internal/database/migrations/20260106102657_alter_daily_checklists_checklist_id_nullable.down.sql new file mode 100644 index 00000000..a1095689 --- /dev/null +++ b/internal/database/migrations/20260106102657_alter_daily_checklists_checklist_id_nullable.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE daily_checklists + ALTER COLUMN checklist_id SET NOT NULL; diff --git a/internal/database/migrations/20260106102657_alter_daily_checklists_checklist_id_nullable.up.sql b/internal/database/migrations/20260106102657_alter_daily_checklists_checklist_id_nullable.up.sql new file mode 100644 index 00000000..2f804e4b --- /dev/null +++ b/internal/database/migrations/20260106102657_alter_daily_checklists_checklist_id_nullable.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE daily_checklists + ALTER COLUMN checklist_id DROP NOT NULL; diff --git a/internal/database/migrations/20260106111217_update_daily_checklist_phases_fk.down.sql b/internal/database/migrations/20260106111217_update_daily_checklist_phases_fk.down.sql new file mode 100644 index 00000000..e2b34f4e --- /dev/null +++ b/internal/database/migrations/20260106111217_update_daily_checklist_phases_fk.down.sql @@ -0,0 +1,4 @@ +ALTER TABLE daily_checklist_phases + DROP CONSTRAINT IF EXISTS fk_dcp_daily_checklist, + ADD CONSTRAINT fk_dcp_checklist + FOREIGN KEY (checklist_id) REFERENCES checklists(id) ON DELETE CASCADE; diff --git a/internal/database/migrations/20260106111217_update_daily_checklist_phases_fk.up.sql b/internal/database/migrations/20260106111217_update_daily_checklist_phases_fk.up.sql new file mode 100644 index 00000000..5f4384b4 --- /dev/null +++ b/internal/database/migrations/20260106111217_update_daily_checklist_phases_fk.up.sql @@ -0,0 +1,4 @@ +ALTER TABLE daily_checklist_phases + DROP CONSTRAINT IF EXISTS fk_dcp_checklist, + ADD CONSTRAINT fk_dcp_daily_checklist + FOREIGN KEY (checklist_id) REFERENCES daily_checklists(id) ON DELETE CASCADE; diff --git a/internal/database/migrations/20260106113936_update_daily_checklist_activity_task_fk.down.sql b/internal/database/migrations/20260106113936_update_daily_checklist_activity_task_fk.down.sql new file mode 100644 index 00000000..e37f1ad0 --- /dev/null +++ b/internal/database/migrations/20260106113936_update_daily_checklist_activity_task_fk.down.sql @@ -0,0 +1,4 @@ +ALTER TABLE daily_checklist_activity_tasks + DROP CONSTRAINT IF EXISTS fk_dcat_daily_checklist, + ADD CONSTRAINT fk_dcat_checklist + FOREIGN KEY (checklist_id) REFERENCES checklists(id) ON DELETE CASCADE; diff --git a/internal/database/migrations/20260106113936_update_daily_checklist_activity_task_fk.up.sql b/internal/database/migrations/20260106113936_update_daily_checklist_activity_task_fk.up.sql new file mode 100644 index 00000000..337ea821 --- /dev/null +++ b/internal/database/migrations/20260106113936_update_daily_checklist_activity_task_fk.up.sql @@ -0,0 +1,4 @@ +ALTER TABLE daily_checklist_activity_tasks + DROP CONSTRAINT IF EXISTS fk_dcat_checklist, + ADD CONSTRAINT fk_dcat_daily_checklist + FOREIGN KEY (checklist_id) REFERENCES daily_checklists(id) ON DELETE CASCADE; diff --git a/internal/database/migrations/20260106150814_add_unique_activity_task_assignments.down.sql b/internal/database/migrations/20260106150814_add_unique_activity_task_assignments.down.sql new file mode 100644 index 00000000..921645e0 --- /dev/null +++ b/internal/database/migrations/20260106150814_add_unique_activity_task_assignments.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE daily_checklist_activity_task_assignments + DROP CONSTRAINT IF EXISTS daily_checklist_activity_task_assignments_task_employee_key; diff --git a/internal/database/migrations/20260106150814_add_unique_activity_task_assignments.up.sql b/internal/database/migrations/20260106150814_add_unique_activity_task_assignments.up.sql new file mode 100644 index 00000000..b4fd9e18 --- /dev/null +++ b/internal/database/migrations/20260106150814_add_unique_activity_task_assignments.up.sql @@ -0,0 +1,3 @@ +ALTER TABLE daily_checklist_activity_task_assignments + ADD CONSTRAINT daily_checklist_activity_task_assignments_task_employee_key + UNIQUE (task_id, employee_id); diff --git a/internal/database/migrations/20260106164640_add_deleted_at_to_master_tables.down.sql b/internal/database/migrations/20260106164640_add_deleted_at_to_master_tables.down.sql new file mode 100644 index 00000000..fb17404d --- /dev/null +++ b/internal/database/migrations/20260106164640_add_deleted_at_to_master_tables.down.sql @@ -0,0 +1,8 @@ +ALTER TABLE phase_activities + DROP COLUMN IF EXISTS deleted_at; + +ALTER TABLE phases + DROP COLUMN IF EXISTS deleted_at; + +ALTER TABLE employees + DROP COLUMN IF EXISTS deleted_at; diff --git a/internal/database/migrations/20260106164640_add_deleted_at_to_master_tables.up.sql b/internal/database/migrations/20260106164640_add_deleted_at_to_master_tables.up.sql new file mode 100644 index 00000000..0fdf6531 --- /dev/null +++ b/internal/database/migrations/20260106164640_add_deleted_at_to_master_tables.up.sql @@ -0,0 +1,8 @@ +ALTER TABLE employees + ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ; + +ALTER TABLE phases + ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ; + +ALTER TABLE phase_activities + ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ; diff --git a/internal/entities/daily-checklist.go b/internal/entities/daily-checklist.go new file mode 100644 index 00000000..8b62b1a3 --- /dev/null +++ b/internal/entities/daily-checklist.go @@ -0,0 +1,81 @@ +package entities + +import "time" + +type DailyChecklist struct { + Id uint `gorm:"primaryKey"` + KandangId uint `gorm:"not null"` + ChecklistId *uint + Date time.Time `gorm:"type:date;not null"` + Name *string `gorm:"type:varchar(255)"` + Status *string `gorm:"type:varchar(255)"` + Category string `gorm:"type:category_code;not null"` + TotalScore *int + DocumentPath *string + RejectReason *string + CreatedBy *uint + CreatedAt time.Time `gorm:"autoCreateTime"` + UpdatedAt time.Time `gorm:"autoUpdateTime"` + + Kandang Kandang `gorm:"foreignKey:KandangId;references:Id"` + Checklist *Checklist `gorm:"foreignKey:ChecklistId;references:Id"` + Creator *User `gorm:"foreignKey:CreatedBy;references:Id"` + Tasks []DailyChecklistTask `gorm:"foreignKey:DailyChecklistId;references:Id"` +} + +type DailyChecklistPhase struct { + Id uint `gorm:"primaryKey"` + ChecklistId uint `gorm:"not null"` + PhaseId uint `gorm:"not null"` + CreatedAt time.Time `gorm:"autoCreateTime"` + + Checklist Checklist `gorm:"foreignKey:ChecklistId;references:Id"` + Phase Phases `gorm:"foreignKey:PhaseId;references:Id"` +} + +type DailyChecklistActivityTask struct { + Id uint `gorm:"primaryKey"` + ChecklistId uint `gorm:"not null"` + PhaseId uint `gorm:"not null"` + PhaseActivityId uint `gorm:"not null"` + TimeType *string `gorm:"type:text"` + Notes *string `gorm:"type:text"` + CreatedAt time.Time `gorm:"autoCreateTime"` + UpdatedAt time.Time `gorm:"autoUpdateTime"` + + Checklist DailyChecklist `gorm:"foreignKey:ChecklistId;references:Id"` + Phase Phases `gorm:"foreignKey:PhaseId;references:Id"` + PhaseActivity PhaseActivity `gorm:"foreignKey:PhaseActivityId;references:Id"` + Assignments []DailyChecklistActivityTaskAssignment `gorm:"foreignKey:TaskId;references:Id"` +} + +type DailyChecklistActivityTaskAssignment struct { + Id uint `gorm:"primaryKey"` + TaskId uint `gorm:"not null"` + EmployeeId uint `gorm:"not null"` + Checked bool `gorm:"not null;default:false"` + Note *string `gorm:"type:text"` + CreatedAt time.Time `gorm:"autoCreateTime"` + UpdatedAt time.Time `gorm:"autoUpdateTime"` + + Task DailyChecklistActivityTask `gorm:"foreignKey:TaskId;references:Id"` + Employee Employee `gorm:"foreignKey:EmployeeId;references:Id"` +} + +type DailyChecklistTask struct { + Id uint `gorm:"primaryKey"` + DailyChecklistId uint `gorm:"not null"` + ChecklistId uint `gorm:"not null"` + ChecklistItemId *uint + IsCompleted bool `gorm:"not null;default:false"` + ScoreValue *int + Notes *string `gorm:"type:text"` + PhotoProof *string + Status *string + CreatedAt time.Time `gorm:"autoCreateTime"` + UpdatedAt time.Time `gorm:"autoUpdateTime"` + + DailyChecklist *DailyChecklist `gorm:"foreignKey:DailyChecklistId;references:Id"` + Checklist Checklist `gorm:"foreignKey:ChecklistId;references:Id"` + ChecklistItem *PhaseActivity `gorm:"foreignKey:ChecklistItemId;references:Id"` +} diff --git a/internal/entities/employee.go b/internal/entities/employee.go index 5810c6ee..a93cbb46 100644 --- a/internal/entities/employee.go +++ b/internal/entities/employee.go @@ -1,13 +1,18 @@ package entities -import "time" +import ( + "time" + + "gorm.io/gorm" +) type Employee struct { - Id uint `gorm:"primaryKey"` - Name string `gorm:"not null"` - IsActive bool `gorm:"not null"` - CreatedAt time.Time `gorm:"autoCreateTime"` - UpdatedAt time.Time `gorm:"autoUpdateTime"` + Id uint `gorm:"primaryKey"` + Name string `gorm:"not null"` + IsActive bool `gorm:"not null"` + CreatedAt time.Time `gorm:"autoCreateTime"` + UpdatedAt time.Time `gorm:"autoUpdateTime"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` EmployeeKandangs []EmployeeKandang `gorm:"foreignKey:EmployeeId;references:Id"` } diff --git a/internal/entities/phase.go b/internal/entities/phase.go index d30369eb..178ed695 100644 --- a/internal/entities/phase.go +++ b/internal/entities/phase.go @@ -7,25 +7,27 @@ import ( ) type Phases struct { - Id uint `gorm:"primaryKey"` - Name string `gorm:"not null"` - IsActive bool `gorm:"not null;default:true"` - Category string `gorm:"type:category_code;not null"` - CreatedAt time.Time `gorm:"autoCreateTime"` + Id uint `gorm:"primaryKey"` + Name string `gorm:"not null"` + IsActive bool `gorm:"not null;default:true"` + Category string `gorm:"type:category_code;not null"` + CreatedAt time.Time `gorm:"autoCreateTime"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` Activities []PhaseActivity `gorm:"foreignKey:PhaseId;references:Id"` } type PhaseActivity struct { - Id uint `gorm:"primaryKey"` - PhaseId uint `gorm:"not null"` - Name string `gorm:"not null"` - Description *string `gorm:"type:text"` - TimeType *string `gorm:"type:text"` - CreatedAt time.Time `gorm:"autoCreateTime"` - UpdatedAt time.Time `gorm:"autoUpdateTime"` + Id uint `gorm:"primaryKey"` + PhaseId uint `gorm:"not null"` + Name string `gorm:"not null"` + Description *string `gorm:"type:text"` + TimeType *string `gorm:"type:text"` + CreatedAt time.Time `gorm:"autoCreateTime"` + UpdatedAt time.Time `gorm:"autoUpdateTime"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` - Phase Phase `gorm:"foreignKey:PhaseId;references:Id"` + Phase Phases `gorm:"foreignKey:PhaseId;references:Id"` } type Checklist struct { @@ -37,5 +39,5 @@ type Checklist struct { UpdatedAt time.Time `gorm:"autoUpdateTime"` DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` - Phase *Phase `gorm:"foreignKey:PhaseId;references:Id"` + Phase *Phases `gorm:"foreignKey:PhaseId;references:Id"` } diff --git a/internal/modules/daily-checklists/controllers/daily-checklist.controller.go b/internal/modules/daily-checklists/controllers/daily-checklist.controller.go new file mode 100644 index 00000000..b5a9b7b5 --- /dev/null +++ b/internal/modules/daily-checklists/controllers/daily-checklist.controller.go @@ -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, + }) +} diff --git a/internal/modules/daily-checklists/dto/daily-checklist.dto.go b/internal/modules/daily-checklists/dto/daily-checklist.dto.go new file mode 100644 index 00000000..31953def --- /dev/null +++ b/internal/modules/daily-checklists/dto/daily-checklist.dto.go @@ -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), + } +} diff --git a/internal/modules/daily-checklists/module.go b/internal/modules/daily-checklists/module.go new file mode 100644 index 00000000..bc82d5f6 --- /dev/null +++ b/internal/modules/daily-checklists/module.go @@ -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) +} diff --git a/internal/modules/daily-checklists/repositories/daily-checklist.repository.go b/internal/modules/daily-checklists/repositories/daily-checklist.repository.go new file mode 100644 index 00000000..e653ba3b --- /dev/null +++ b/internal/modules/daily-checklists/repositories/daily-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 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), + } +} diff --git a/internal/modules/daily-checklists/route.go b/internal/modules/daily-checklists/route.go new file mode 100644 index 00000000..c8542671 --- /dev/null +++ b/internal/modules/daily-checklists/route.go @@ -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) +} diff --git a/internal/modules/daily-checklists/services/daily-checklist.service.go b/internal/modules/daily-checklists/services/daily-checklist.service.go new file mode 100644 index 00000000..bf5320e6 --- /dev/null +++ b/internal/modules/daily-checklists/services/daily-checklist.service.go @@ -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 +} diff --git a/internal/modules/daily-checklists/validations/daily-checklist.validation.go b/internal/modules/daily-checklists/validations/daily-checklist.validation.go new file mode 100644 index 00000000..ba81fd0d --- /dev/null +++ b/internal/modules/daily-checklists/validations/daily-checklist.validation.go @@ -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"` +} diff --git a/internal/modules/master/employees/services/employees.service.go b/internal/modules/master/employees/services/employees.service.go index aa82255d..4998eaec 100644 --- a/internal/modules/master/employees/services/employees.service.go +++ b/internal/modules/master/employees/services/employees.service.go @@ -2,8 +2,6 @@ package service import ( "errors" - "fmt" - "strconv" "strings" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" @@ -41,10 +39,8 @@ func NewEmployeesService(repo repository.EmployeesRepository, validate *validato func (s employeesService) withRelations(db *gorm.DB) *gorm.DB { return db. - Preload("EmployeeKandangs.Kandang") - // Preload("EmployeeKandangs.Kandang.Location"). - // Preload("EmployeeKandangs.Kandang.Pic"). - // Preload("EmployeeKandangs.Kandang.CreatedUser") + Preload("EmployeeKandangs.Kandang"). + Where("employees.deleted_at IS NULL") } func (s employeesService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Employees, int64, error) { @@ -98,9 +94,9 @@ func (s *employeesService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent return nil, fiber.NewError(fiber.StatusBadRequest, "name cannot be empty") } - kandangIDs, err := parseKandangIDs(req.KandangIDs) - if err != nil { - return nil, fiber.NewError(fiber.StatusBadRequest, err.Error()) + 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 { @@ -181,9 +177,9 @@ func (s employeesService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin } if req.KandangIDs != nil { - ids, err := parseKandangIDs(*req.KandangIDs) - if err != nil { - return nil, fiber.NewError(fiber.StatusBadRequest, err.Error()) + 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 @@ -248,33 +244,22 @@ func (s employeesService) DeleteOne(c *fiber.Ctx, id uint) error { return nil } -func parseKandangIDs(raw string) ([]uint, error) { - parts := strings.Split(raw, ",") - ids := make([]uint, 0, len(parts)) +func normalizeKandangIDs(ids []uint) []uint { + result := make([]uint, 0, len(ids)) seen := make(map[uint]struct{}) - for _, part := range parts { - value := strings.TrimSpace(part) - if value == "" { + for _, id := range ids { + if id == 0 { continue } - parsed, err := strconv.ParseUint(value, 10, 64) - if err != nil { - return nil, fmt.Errorf("invalid kandang id: %s", value) - } - - id := uint(parsed) if _, ok := seen[id]; ok { continue } + seen[id] = struct{}{} - ids = append(ids, id) + result = append(result, id) } - if len(ids) == 0 { - return nil, errors.New("kandang_ids must contain at least one valid id") - } - - return ids, nil + return result } diff --git a/internal/modules/master/employees/validations/employees.validation.go b/internal/modules/master/employees/validations/employees.validation.go index 159b875f..2e2cc879 100644 --- a/internal/modules/master/employees/validations/employees.validation.go +++ b/internal/modules/master/employees/validations/employees.validation.go @@ -2,13 +2,13 @@ package validation type Create struct { Name string `json:"name" validate:"required_strict,min=3"` - KandangIDs string `json:"kandang_ids" validate:"required"` + 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 *string `json:"kandang_ids,omitempty"` + KandangIDs *[]uint `json:"kandang_ids,omitempty" validate:"omitempty,min=1,dive,required"` IsActive *bool `json:"is_active,omitempty"` } diff --git a/internal/route/route.go b/internal/route/route.go index 877ec875..519ea5aa 100644 --- a/internal/route/route.go +++ b/internal/route/route.go @@ -11,6 +11,7 @@ import ( approvals "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals" closings "gitlab.com/mbugroup/lti-api.git/internal/modules/closings" constants "gitlab.com/mbugroup/lti-api.git/internal/modules/constants" + dailyChecklists "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists" expenses "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses" finance "gitlab.com/mbugroup/lti-api.git/internal/modules/finance" inventory "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory" @@ -46,6 +47,7 @@ func Routes(app *fiber.App, db *gorm.DB) { closings.ClosingModule{}, repports.RepportModule{}, finance.FinanceModule{}, + dailyChecklists.DailyChecklistModule{}, // MODULE REGISTRY } From f5a016b74b6183cad4e2c75135c3febb38eb4ab7 Mon Sep 17 00:00:00 2001 From: MacBook Air M1 Date: Tue, 6 Jan 2026 17:23:06 +0700 Subject: [PATCH 10/10] adjust init population --- .../closings/services/closing.service.go | 2 +- .../repports/services/repport.service.go | 35 ++++++++++--------- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/internal/modules/closings/services/closing.service.go b/internal/modules/closings/services/closing.service.go index 47e30a7f..ddf52b49 100644 --- a/internal/modules/closings/services/closing.service.go +++ b/internal/modules/closings/services/closing.service.go @@ -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 } } diff --git a/internal/modules/repports/services/repport.service.go b/internal/modules/repports/services/repport.service.go index 9f54fad8..ebf68867 100644 --- a/internal/modules/repports/services/repport.service.go +++ b/internal/modules/repports/services/repport.service.go @@ -318,7 +318,8 @@ func mapRecordingToProductionResultDTO(record entity.Recording) dto.ProductionRe result.Fi = float64(*record.CumIntake) } - avgWeight := calculateAverageBodyWeight(record.BodyWeights) + // avgWeight := calculateAverageBodyWeight(record.BodyWeights) + avgWeight := 1.0 if avgWeight > 0 { result.Bw = avgWeight } @@ -350,25 +351,25 @@ func mapRecordingToProductionResultDTO(record entity.Recording) dto.ProductionRe return result } -func calculateAverageBodyWeight(bodyWeights []entity.RecordingBW) float64 { - var totalQty float64 - var totalWeight float64 +// 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 - } - } +// 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 - } +// if totalQty == 0 { +// return 0 +// } - return totalWeight / totalQty -} +// return totalWeight / totalQty +// } type eggSummary struct { TotalQty int64