From c3f8ae5887c44470a662e8aa5ba9568abc486c8c Mon Sep 17 00:00:00 2001 From: MacBook Air M1 Date: Wed, 7 Jan 2026 17:39:17 +0700 Subject: [PATCH] add api daily checklist report --- .../controllers/daily-checklist.controller.go | 166 +++++- .../dto/daily-checklist.dto.go | 79 ++- internal/modules/daily-checklists/route.go | 9 +- .../services/daily-checklist.service.go | 472 +++++++++++++++++- .../validations/daily-checklist.validation.go | 22 +- .../services/phase-activity.service.go | 3 +- 6 files changed, 704 insertions(+), 47 deletions(-) diff --git a/internal/modules/daily-checklists/controllers/daily-checklist.controller.go b/internal/modules/daily-checklists/controllers/daily-checklist.controller.go index 7f2aabe9..7c92664a 100644 --- a/internal/modules/daily-checklists/controllers/daily-checklist.controller.go +++ b/internal/modules/daily-checklists/controllers/daily-checklist.controller.go @@ -7,6 +7,7 @@ import ( "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" + kandangDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto" "gitlab.com/mbugroup/lti-api.git/internal/response" "github.com/gofiber/fiber/v2" @@ -28,6 +29,18 @@ func (u *DailyChecklistController) GetAll(c *fiber.Ctx) error { Limit: c.QueryInt("limit", 10), Search: c.Query("search", ""), } + query.DateFrom = c.Query("date_from", "") + query.DateTo = c.Query("date_to", "") + query.Status = c.Query("status", "") + + if kandangParam := c.Query("kandang_id", ""); kandangParam != "" { + kandangID, err := strconv.ParseUint(kandangParam, 10, 64) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid kandang_id") + } + value := uint(kandangID) + query.KandangID = &value + } if query.Page < 1 || query.Limit < 1 { return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0") @@ -38,6 +51,40 @@ func (u *DailyChecklistController) GetAll(c *fiber.Ctx) error { return err } + responseData := make([]dto.DailyChecklistListDTO, len(result)) + for i, item := range result { + var name string + if item.Name != nil { + name = *item.Name + } + + var status string + if item.Status != nil { + status = *item.Status + } + + var kandang *kandangDTO.KandangRelationDTO + if item.Kandang.Id != 0 { + mapped := kandangDTO.ToKandangRelationDTO(item.Kandang) + kandang = &mapped + } + + responseData[i] = dto.DailyChecklistListDTO{ + Id: item.ID, + Name: name, + Status: status, + Category: item.Category, + Date: item.Date, + Kandang: kandang, + CreatedUser: nil, + CreatedAt: item.CreatedAt, + UpdatedAt: item.UpdatedAt, + TotalPhase: item.TotalPhase, + TotalActivity: item.TotalActivity, + Progress: item.Progress, + } + } + return c.Status(fiber.StatusOK). JSON(response.SuccessWithPaginate[dto.DailyChecklistListDTO]{ Code: fiber.StatusOK, @@ -49,7 +96,7 @@ func (u *DailyChecklistController) GetAll(c *fiber.Ctx) error { TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))), TotalResults: totalResults, }, - Data: dto.ToDailyChecklistListDTOs(result), + Data: responseData, }) } @@ -128,6 +175,121 @@ func (u *DailyChecklistController) GetSummary(c *fiber.Ctx) error { }) } +func (u *DailyChecklistController) GetReport(c *fiber.Ctx) error { + query := &validation.ReportQuery{ + Page: c.QueryInt("page", 1), + Limit: c.QueryInt("limit", 10), + Month: c.QueryInt("bulan", 0), + Year: c.QueryInt("tahun", 0), + } + + parseUintParam := func(param string) (*uint, error) { + if param == "" { + return nil, nil + } + value, err := strconv.ParseUint(param, 10, 64) + if err != nil { + return nil, err + } + u := uint(value) + return &u, nil + } + + if val, err := parseUintParam(c.Query("area_id", "")); err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid area_id") + } else { + query.AreaID = val + } + + if val, err := parseUintParam(c.Query("location_id", "")); err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid location_id") + } else { + query.LocationID = val + } + + if val, err := parseUintParam(c.Query("kandang_id", "")); err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid kandang_id") + } else { + query.KandangID = val + } + + if val, err := parseUintParam(c.Query("employee_id", "")); err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid employee_id") + } else { + query.EmployeeID = val + } + + if val, err := parseUintParam(c.Query("phase_id", "")); err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid phase_id") + } else { + query.PhaseID = val + } + + if query.Month == 0 || query.Year == 0 { + return fiber.NewError(fiber.StatusBadRequest, "bulan and tahun are required") + } + + result, totalResults, err := u.DailyChecklistService.GetReport(c, query) + withoutActivities := func(src map[string]int) map[string]int { + if src == nil { + return map[string]int{} + } + return src + } + + if err != nil { + return err + } + + responseData := make([]dto.DailyChecklistReportDTO, len(result)) + for i, item := range result { + responseData[i] = dto.DailyChecklistReportDTO{ + Area: dto.DailyChecklistReportEntityDTO{ + Id: item.AreaID, + Name: item.AreaName, + }, + Farm: dto.DailyChecklistReportEntityDTO{ + Id: item.LocationID, + Name: item.LocationName, + }, + Kandang: dto.DailyChecklistReportEntityDTO{ + Id: item.KandangID, + Name: item.KandangName, + }, + ABK: dto.DailyChecklistReportEntityDTO{ + Id: item.EmployeeID, + Name: item.EmployeeName, + }, + Phase: item.PhaseName, + DailyActivities: withoutActivities(item.DailyActivities), + Summary: dto.DailyChecklistReportSummaryDTO{ + TotalChecklist: item.Summary.TotalChecklist, + JumlahHariEfektif: item.Summary.JumlahHariEfektif, + AbkPercentage: item.Summary.AbkPercentage, + KandangPercentage: item.Summary.KandangPercentage, + Kategori: dto.DailyChecklistReportCategoryDTO{ + Kurang: item.Summary.Category.Kurang, + Cukup: item.Summary.Category.Cukup, + Baik: item.Summary.Category.Baik, + }, + }, + } + } + + return c.Status(fiber.StatusOK). + JSON(response.SuccessWithPaginate[dto.DailyChecklistReportDTO]{ + Code: fiber.StatusOK, + Status: "success", + Message: "Get daily checklist report successfully", + Meta: response.Meta{ + Page: query.Page, + Limit: query.Limit, + TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))), + TotalResults: totalResults, + }, + Data: responseData, + }) +} func (u *DailyChecklistController) GetOne(c *fiber.Ctx) error { param := c.Params("idDailyChecklist") @@ -199,7 +361,7 @@ func (u *DailyChecklistController) UpdateOne(c *fiber.Ctx) error { } func (u *DailyChecklistController) DeleteOne(c *fiber.Ctx) error { - param := c.Params("id") + param := c.Params("idDailyChecklist") id, err := strconv.Atoi(param) if err != nil { diff --git a/internal/modules/daily-checklists/dto/daily-checklist.dto.go b/internal/modules/daily-checklists/dto/daily-checklist.dto.go index eea51184..d133b76e 100644 --- a/internal/modules/daily-checklists/dto/daily-checklist.dto.go +++ b/internal/modules/daily-checklists/dto/daily-checklist.dto.go @@ -19,15 +19,18 @@ type DailyChecklistRelationDTO struct { } type DailyChecklistListDTO struct { - Id uint `json:"id"` - Name string `json:"name"` - Status string `json:"status"` - Category string `json:"category"` - Date time.Time `json:"date"` - Kandang *kandangDTO.KandangRelationDTO `json:"kandang,omitempty"` - CreatedUser *userDTO.UserRelationDTO `json:"created_user"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + Id uint `json:"id"` + Name string `json:"name"` + Status string `json:"status"` + Category string `json:"category"` + Date time.Time `json:"date"` + Kandang *kandangDTO.KandangRelationDTO `json:"kandang,omitempty"` + CreatedUser *userDTO.UserRelationDTO `json:"created_user"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + TotalPhase int `json:"total_phase"` + TotalActivity int `json:"total_activity"` + Progress int `json:"progress"` } type DailyChecklistDetailDTO struct { @@ -59,6 +62,35 @@ type DailyChecklistPerformanceOverviewDTO struct { ActivityLeft int `json:"activity_left"` } +type DailyChecklistReportDTO struct { + Area DailyChecklistReportEntityDTO `json:"area"` + Farm DailyChecklistReportEntityDTO `json:"farm"` + Kandang DailyChecklistReportEntityDTO `json:"kandang"` + ABK DailyChecklistReportEntityDTO `json:"abk"` + Phase string `json:"phase"` + DailyActivities map[string]int `json:"daily_activities"` + Summary DailyChecklistReportSummaryDTO `json:"summary"` +} + +type DailyChecklistReportEntityDTO struct { + Id uint `json:"id"` + Name string `json:"name"` +} + +type DailyChecklistReportSummaryDTO struct { + TotalChecklist int `json:"total_checklist"` + JumlahHariEfektif int `json:"jumlah_hari_efektif"` + AbkPercentage int `json:"abk_percentage"` + KandangPercentage int `json:"kandang_percentage"` + Kategori DailyChecklistReportCategoryDTO `json:"kategori"` +} + +type DailyChecklistReportCategoryDTO struct { + Kurang int `json:"kurang"` + Cukup int `json:"cukup"` + Baik int `json:"baik"` +} + type DailyChecklistPhaseDTO struct { Id uint `json:"id"` PhaseId uint `json:"phase_id"` @@ -121,26 +153,21 @@ func ToDailyChecklistListDTO(e entity.DailyChecklist) DailyChecklistListDTO { } return DailyChecklistListDTO{ - Id: e.Id, - Name: name, - Status: status, - Category: e.Category, - Date: e.Date, - Kandang: kandang, - CreatedAt: e.CreatedAt, - UpdatedAt: e.UpdatedAt, - CreatedUser: createdUser, + Id: e.Id, + Name: name, + Status: status, + Category: e.Category, + Date: e.Date, + Kandang: kandang, + CreatedAt: e.CreatedAt, + UpdatedAt: e.UpdatedAt, + CreatedUser: createdUser, + TotalPhase: 0, + TotalActivity: 0, + Progress: 0, } } -func ToDailyChecklistListDTOs(e []entity.DailyChecklist) []DailyChecklistListDTO { - result := make([]DailyChecklistListDTO, len(e)) - for i, r := range e { - result[i] = ToDailyChecklistListDTO(r) - } - return result -} - func ToDailyChecklistDetailDTO(checklist entity.DailyChecklist, phases []entity.DailyChecklistPhase, tasks []entity.DailyChecklistActivityTask, assignedEmployees []entity.Employee, totalActivities int, progress float64) DailyChecklistDetailDTO { phaseDTOs := make([]DailyChecklistPhaseDTO, 0, len(phases)) for _, phase := range phases { diff --git a/internal/modules/daily-checklists/route.go b/internal/modules/daily-checklists/route.go index 72396092..0f6657c0 100644 --- a/internal/modules/daily-checklists/route.go +++ b/internal/modules/daily-checklists/route.go @@ -1,7 +1,7 @@ package dailyChecklists import ( - m "gitlab.com/mbugroup/lti-api.git/internal/middleware" + // m "gitlab.com/mbugroup/lti-api.git/internal/middleware" controller "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/controllers" dailyChecklist "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/services" user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" @@ -13,12 +13,15 @@ func DailyChecklistRoutes(v1 fiber.Router, u user.UserService, s dailyChecklist. ctrl := controller.NewDailyChecklistController(s) route := v1.Group("/daily-checklists") - route.Use(m.Auth(u)) + // route.Use(m.Auth(u)) route.Get("/", ctrl.GetAll) + route.Get("/report", ctrl.GetReport) route.Get("/summary", ctrl.GetSummary) + route.Get("/report", ctrl.GetReport) + // create daily checklist route.Post("/", ctrl.CreateOne) @@ -56,5 +59,5 @@ func DailyChecklistRoutes(v1 fiber.Router, u user.UserService, s dailyChecklist. route.Post("/assignment", ctrl.UpdateAssignment) route.Patch("/:idDailyChecklist", ctrl.UpdateOne) - route.Delete("/:id", ctrl.DeleteOne) + route.Delete("/:idDailyChecklist", ctrl.DeleteOne) } diff --git a/internal/modules/daily-checklists/services/daily-checklist.service.go b/internal/modules/daily-checklists/services/daily-checklist.service.go index 067b39b3..3f23f6a3 100644 --- a/internal/modules/daily-checklists/services/daily-checklist.service.go +++ b/internal/modules/daily-checklists/services/daily-checklist.service.go @@ -22,7 +22,7 @@ import ( ) type DailyChecklistService interface { - GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.DailyChecklist, int64, error) + GetAll(ctx *fiber.Ctx, params *validation.Query) ([]DailyChecklistListItem, int64, error) GetOne(ctx *fiber.Ctx, id uint) (*entity.DailyChecklist, error) CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.DailyChecklist, error) UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.DailyChecklist, error) @@ -35,6 +35,7 @@ type DailyChecklistService interface { GetChecklistPhaseIDs(ctx *fiber.Ctx, checklistID uint) ([]uint, error) GetDetail(ctx *fiber.Ctx, id uint) (*DailyChecklistDetail, error) GetSummary(ctx *fiber.Ctx, params *validation.SummaryQuery) ([]DailyChecklistSummary, error) + GetReport(ctx *fiber.Ctx, params *validation.ReportQuery) ([]DailyChecklistReportItem, int64, error) } type dailyChecklistService struct { @@ -53,6 +54,20 @@ type DailyChecklistDetail struct { Progress float64 } +type DailyChecklistListItem struct { + ID uint + Name *string + Date time.Time + Category string + Status *string + CreatedAt time.Time + UpdatedAt time.Time + Kandang entity.Kandang + TotalPhase int + TotalActivity int + Progress int +} + type DailyChecklistSummary struct { EmployeeID uint EmployeeName string @@ -65,6 +80,34 @@ type DailyChecklistSummary struct { LastActivity *time.Time } +type DailyChecklistReportItem struct { + AreaID uint + AreaName string + LocationID uint + LocationName string + KandangID uint + KandangName string + EmployeeID uint + EmployeeName string + PhaseName string + DailyActivities map[string]int + Summary DailyChecklistReportSummary +} + +type DailyChecklistReportSummary struct { + TotalChecklist int + JumlahHariEfektif int + AbkPercentage int + KandangPercentage int + Category DailyChecklistReportCategory +} + +type DailyChecklistReportCategory struct { + Kurang int + Cukup int + Baik int +} + func NewDailyChecklistService(repo repository.DailyChecklistRepository, phaseRepo phaseRepo.PhasesRepository, validate *validator.Validate) DailyChecklistService { return &dailyChecklistService{ Log: utils.Log, @@ -78,26 +121,160 @@ func (s dailyChecklistService) withRelations(db *gorm.DB) *gorm.DB { return db.Preload("Kandang") } -func (s dailyChecklistService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.DailyChecklist, int64, error) { +func (s dailyChecklistService) GetAll(c *fiber.Ctx, params *validation.Query) ([]DailyChecklistListItem, int64, error) { if err := s.Validate.Struct(params); err != nil { 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") - }) + db := s.Repository.DB().WithContext(c.Context()). + Table("daily_checklists dc"). + Joins("JOIN kandangs k ON k.id = dc.kandang_id") - if err != nil { + if params.DateFrom != "" { + dateFrom, err := time.Parse("2006-01-02", params.DateFrom) + if err != nil { + return nil, 0, fiber.NewError(fiber.StatusBadRequest, "invalid date_from format, use YYYY-MM-DD") + } + db = db.Where("dc.date >= ?", dateFrom) + } + + if params.DateTo != "" { + dateTo, err := time.Parse("2006-01-02", params.DateTo) + if err != nil { + return nil, 0, fiber.NewError(fiber.StatusBadRequest, "invalid date_to format, use YYYY-MM-DD") + } + db = db.Where("dc.date <= ?", dateTo) + } + + if params.KandangID != nil { + db = db.Where("dc.kandang_id = ?", *params.KandangID) + } + + if params.Status != "" { + db = db.Where("dc.status = ?", params.Status) + } + + if params.Search != "" { + like := "%" + params.Search + "%" + db = db.Where("(k.name ILIKE ? OR dc.category ILIKE ?)", like, like) + } + + countDB := db.Session(&gorm.Session{}) + var total int64 + if err := countDB.Count(&total).Error; err != nil { + s.Log.Errorf("Failed to count dailyChecklists: %+v", err) + return nil, 0, err + } + + type dailyChecklistListRow struct { + ID uint + Name *string + Date time.Time + Category string + Status *string + CreatedAt time.Time + UpdatedAt time.Time + KandangID uint + TotalPhase int64 + TotalActivity int64 + TotalAssignments int64 + CompletedAssignments int64 + } + + rows := make([]dailyChecklistListRow, 0) + selectDB := db.Session(&gorm.Session{}) + if err := selectDB. + Select(` + dc.id, + dc.name, + dc.date, + dc.category, + dc.status, + dc.created_at, + dc.updated_at, + dc.kandang_id, + COALESCE(( + SELECT COUNT(*) + FROM daily_checklist_phases dcp + WHERE dcp.checklist_id = dc.id + ), 0) AS total_phase, + COALESCE(( + SELECT COUNT(pa.id) + FROM daily_checklist_phases dcp + JOIN phase_activities pa ON pa.phase_id = dcp.phase_id + WHERE dcp.checklist_id = dc.id AND pa.deleted_at IS NULL + ), 0) AS total_activity, + COALESCE(( + SELECT COUNT(*) + FROM daily_checklist_activity_task_assignments dca + JOIN daily_checklist_activity_tasks dcat ON dcat.id = dca.task_id + WHERE dcat.checklist_id = dc.id + ), 0) AS total_assignments, + COALESCE(( + SELECT COUNT(*) + FROM daily_checklist_activity_task_assignments dca + JOIN daily_checklist_activity_tasks dcat ON dcat.id = dca.task_id + WHERE dcat.checklist_id = dc.id AND dca.checked + ), 0) AS completed_assignments`). + Order("dc.date DESC, dc.created_at DESC"). + Offset(offset). + Limit(params.Limit). + Scan(&rows).Error; err != nil { s.Log.Errorf("Failed to get dailyChecklists: %+v", err) return nil, 0, err } - return dailyChecklists, total, nil + + kandangIDs := make([]uint, 0, len(rows)) + seen := make(map[uint]struct{}) + for _, row := range rows { + if _, ok := seen[row.KandangID]; !ok { + seen[row.KandangID] = struct{}{} + kandangIDs = append(kandangIDs, row.KandangID) + } + } + + kandangMap := make(map[uint]entity.Kandang) + if len(kandangIDs) > 0 { + var kandangs []entity.Kandang + if err := s.Repository.DB().WithContext(c.Context()). + Where("id IN ?", kandangIDs). + Preload("Location"). + Preload("Pic"). + Preload("CreatedUser"). + Find(&kandangs).Error; err != nil { + s.Log.Errorf("Failed to get kandangs for daily checklist list: %+v", err) + return nil, 0, err + } + for _, kandang := range kandangs { + kandangMap[kandang.Id] = kandang + } + } + + items := make([]DailyChecklistListItem, len(rows)) + for i, row := range rows { + progress := 0 + if row.TotalAssignments > 0 { + progress = int(math.Round(float64(row.CompletedAssignments) / float64(row.TotalAssignments) * 100)) + } + + items[i] = DailyChecklistListItem{ + ID: row.ID, + Name: row.Name, + Date: row.Date, + Category: row.Category, + Status: row.Status, + CreatedAt: row.CreatedAt, + UpdatedAt: row.UpdatedAt, + Kandang: kandangMap[row.KandangID], + TotalPhase: int(row.TotalPhase), + TotalActivity: int(row.TotalActivity), + Progress: progress, + } + } + + return items, total, nil } func (s dailyChecklistService) GetOne(c *fiber.Ctx, id uint) (*entity.DailyChecklist, error) { @@ -633,7 +810,7 @@ func (s dailyChecklistService) GetSummary(c *fiber.Ctx, params *validation.Summa Joins("JOIN daily_checklists d ON d.id = t.checklist_id"). Joins("JOIN kandangs k ON k.id = d.kandang_id"). Joins("JOIN employees e ON e.id = a.employee_id"). - Where("d.date BETWEEN ? AND ?", dateFrom, dateTo) + Where("d.date BETWEEN ? AND ? AND d.status = ?", dateFrom, dateTo, "APPROVED") if params.Category != "" { db = db.Where("d.category = ?", params.Category) @@ -673,3 +850,274 @@ func (s dailyChecklistService) GetSummary(c *fiber.Ctx, params *validation.Summa return summaries, nil } + +func (s dailyChecklistService) GetReport(c *fiber.Ctx, params *validation.ReportQuery) ([]DailyChecklistReportItem, int64, error) { + if err := s.Validate.Struct(params); err != nil { + return nil, 0, err + } + + offset := (params.Page - 1) * params.Limit + + buildBase := func() *gorm.DB { + db := s.Repository.DB().WithContext(c.Context()). + Table("daily_checklist_activity_task_assignments AS dca"). + Joins("JOIN daily_checklist_activity_tasks dcat ON dcat.id = dca.task_id"). + Joins("JOIN daily_checklists dc ON dc.id = dcat.checklist_id"). + Joins("JOIN employees e ON e.id = dca.employee_id"). + Joins("JOIN kandangs k ON k.id = dc.kandang_id"). + Joins("JOIN locations loc ON loc.id = k.location_id"). + Joins("JOIN areas a ON a.id = loc.area_id"). + Joins("JOIN phases p ON p.id = dcat.phase_id"). + Where("EXTRACT(MONTH FROM dc.date) = ?", params.Month). + Where("EXTRACT(YEAR FROM dc.date) = ?", params.Year) + + if params.AreaID != nil { + db = db.Where("a.id = ?", *params.AreaID) + } + if params.LocationID != nil { + db = db.Where("loc.id = ?", *params.LocationID) + } + if params.KandangID != nil { + db = db.Where("k.id = ?", *params.KandangID) + } + if params.EmployeeID != nil { + db = db.Where("dca.employee_id = ?", *params.EmployeeID) + } + if params.PhaseID != nil { + db = db.Where("p.id = ?", *params.PhaseID) + } + return db + } + + buildGroupedQuery := func() *gorm.DB { + return buildBase(). + Select(` + a.id AS area_id, + a.name AS area_name, + loc.id AS location_id, + loc.name AS location_name, + k.id AS kandang_id, + k.name AS kandang_name, + e.id AS employee_id, + e.name AS employee_name, + p.id AS phase_id, + p.name AS phase_name, + SUM(CASE WHEN dca.checked THEN 1 ELSE 0 END) AS completed_assignments, + COUNT(*) AS total_assignments`). + Group("a.id, a.name, loc.id, loc.name, k.id, k.name, e.id, e.name, p.id, p.name") + } + + var total int64 + groupedForCount := buildGroupedQuery() + if err := s.Repository.DB().WithContext(c.Context()). + Table("(?) AS grouped", groupedForCount). + Count(&total).Error; err != nil { + s.Log.Errorf("Failed to count report data: %+v", err) + return nil, 0, err + } + + type reportRow struct { + AreaID uint + AreaName string + LocationID uint + LocationName string + KandangID uint + KandangName string + EmployeeID uint + EmployeeName string + PhaseID uint + PhaseName string + CompletedAssignments int64 + TotalAssignments int64 + } + + rows := make([]reportRow, 0) + if err := buildGroupedQuery(). + Order("a.name, loc.name, k.name, e.name"). + Offset(offset). + Limit(params.Limit). + Scan(&rows).Error; err != nil { + s.Log.Errorf("Failed to fetch report data: %+v", err) + return nil, 0, err + } + + if len(rows) == 0 { + return []DailyChecklistReportItem{}, total, nil + } + + type comboKey struct { + EmployeeID uint + KandangID uint + PhaseID uint + } + + employeeIDs := make([]uint, 0) + kandangIDs := make([]uint, 0) + phaseIDs := make([]uint, 0) + comboSet := make(map[comboKey]struct{}) + employeeSet := make(map[uint]struct{}) + kandangSet := make(map[uint]struct{}) + phaseSet := make(map[uint]struct{}) + + for _, row := range rows { + key := comboKey{EmployeeID: row.EmployeeID, KandangID: row.KandangID, PhaseID: row.PhaseID} + comboSet[key] = struct{}{} + if _, ok := employeeSet[row.EmployeeID]; !ok { + employeeSet[row.EmployeeID] = struct{}{} + employeeIDs = append(employeeIDs, row.EmployeeID) + } + if _, ok := kandangSet[row.KandangID]; !ok { + kandangSet[row.KandangID] = struct{}{} + kandangIDs = append(kandangIDs, row.KandangID) + } + if _, ok := phaseSet[row.PhaseID]; !ok { + phaseSet[row.PhaseID] = struct{}{} + phaseIDs = append(phaseIDs, row.PhaseID) + } + } + + dailyActivityMap := make(map[comboKey]map[string]int) + if len(employeeIDs) > 0 { + var dailyRows []struct { + EmployeeID uint + KandangID uint + PhaseID uint + Date time.Time + Completed int64 + } + + dailyQuery := buildBase(). + Where("dca.employee_id IN ?", employeeIDs). + Where("dc.kandang_id IN ?", kandangIDs). + Where("dcat.phase_id IN ?", phaseIDs). + Select(` + dca.employee_id, + dc.kandang_id, + dcat.phase_id, + dc.date, + SUM(CASE WHEN dca.checked THEN 1 ELSE 0 END) AS completed`). + Group("dca.employee_id, dc.kandang_id, dcat.phase_id, dc.date") + + if err := dailyQuery.Scan(&dailyRows).Error; err != nil { + s.Log.Errorf("Failed to fetch daily activities for report: %+v", err) + return nil, 0, err + } + + for _, row := range dailyRows { + key := comboKey{EmployeeID: row.EmployeeID, KandangID: row.KandangID, PhaseID: row.PhaseID} + if _, ok := comboSet[key]; !ok { + continue + } + if _, ok := dailyActivityMap[key]; !ok { + dailyActivityMap[key] = make(map[string]int) + } + day := strconv.Itoa(row.Date.Day()) + dailyActivityMap[key][day] = int(row.Completed) + } + } + + employeeStats := make(map[uint]struct { + Completed int64 + Total int64 + }) + var employeeRows []struct { + EmployeeID uint + Completed int64 + Total int64 + } + if err := buildBase(). + Select(` + dca.employee_id, + SUM(CASE WHEN dca.checked THEN 1 ELSE 0 END) AS completed, + COUNT(*) AS total`). + Group("dca.employee_id"). + Scan(&employeeRows).Error; err != nil { + s.Log.Errorf("Failed to fetch employee stats for report: %+v", err) + return nil, 0, err + } + for _, row := range employeeRows { + employeeStats[row.EmployeeID] = struct { + Completed int64 + Total int64 + }{Completed: row.Completed, Total: row.Total} + } + + kandangStats := make(map[uint]struct { + Completed int64 + Total int64 + }) + var kandangRows []struct { + KandangID uint + Completed int64 + Total int64 + } + if err := buildBase(). + Select(` + dc.kandang_id, + SUM(CASE WHEN dca.checked THEN 1 ELSE 0 END) AS completed, + COUNT(*) AS total`). + Group("dc.kandang_id"). + Scan(&kandangRows).Error; err != nil { + s.Log.Errorf("Failed to fetch kandang stats for report: %+v", err) + return nil, 0, err + } + for _, row := range kandangRows { + kandangStats[row.KandangID] = struct { + Completed int64 + Total int64 + }{Completed: row.Completed, Total: row.Total} + } + + items := make([]DailyChecklistReportItem, len(rows)) + for i, row := range rows { + key := comboKey{EmployeeID: row.EmployeeID, KandangID: row.KandangID, PhaseID: row.PhaseID} + + activities := dailyActivityMap[key] + if activities == nil { + activities = map[string]int{} + } + + totalChecklist := 0 + for _, count := range activities { + totalChecklist += count + } + + employeeStat := employeeStats[row.EmployeeID] + abkPercentage := 0 + if employeeStat.Total > 0 { + abkPercentage = int(math.Round(float64(employeeStat.Completed) / float64(employeeStat.Total) * 100)) + } + + kandangStat := kandangStats[row.KandangID] + kandangPercentage := 0 + if kandangStat.Total > 0 { + kandangPercentage = int(math.Round(float64(kandangStat.Completed) / float64(kandangStat.Total) * 100)) + } + + items[i] = DailyChecklistReportItem{ + AreaID: row.AreaID, + AreaName: row.AreaName, + LocationID: row.LocationID, + LocationName: row.LocationName, + KandangID: row.KandangID, + KandangName: row.KandangName, + EmployeeID: row.EmployeeID, + EmployeeName: row.EmployeeName, + PhaseName: row.PhaseName, + DailyActivities: activities, + Summary: DailyChecklistReportSummary{ + TotalChecklist: totalChecklist, + JumlahHariEfektif: len(activities), + AbkPercentage: abkPercentage, + KandangPercentage: kandangPercentage, + Category: DailyChecklistReportCategory{ + Kurang: 0, + Cukup: 0, + Baik: 0, + }, + }, + } + } + + return items, total, nil +} diff --git a/internal/modules/daily-checklists/validations/daily-checklist.validation.go b/internal/modules/daily-checklists/validations/daily-checklist.validation.go index 969c04e2..a42d424a 100644 --- a/internal/modules/daily-checklists/validations/daily-checklist.validation.go +++ b/internal/modules/daily-checklists/validations/daily-checklist.validation.go @@ -13,9 +13,13 @@ 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"` + DateFrom string `query:"date_from" validate:"omitempty"` + DateTo string `query:"date_to" validate:"omitempty"` + Status string `query:"status" validate:"omitempty"` + KandangID *uint `query:"kandang_id" validate:"omitempty"` } type AssignPhases struct { @@ -39,3 +43,15 @@ type SummaryQuery struct { Category string `query:"category" validate:"omitempty"` KandangID *uint `query:"kandang_id" validate:"omitempty"` } + +type ReportQuery struct { + Page int `query:"page" validate:"required,number,min=1,gt=0"` + Limit int `query:"limit" validate:"required,number,min=1,max=100,gt=0"` + Month int `query:"bulan" validate:"required,number,min=1,max=12"` + Year int `query:"tahun" validate:"required,number,min=1900"` + AreaID *uint `query:"area_id" validate:"omitempty"` + LocationID *uint `query:"location_id" validate:"omitempty"` + KandangID *uint `query:"kandang_id" validate:"omitempty"` + EmployeeID *uint `query:"employee_id" validate:"omitempty"` + PhaseID *uint `query:"phase_id" validate:"omitempty"` +} diff --git a/internal/modules/master/phase-activities/services/phase-activity.service.go b/internal/modules/master/phase-activities/services/phase-activity.service.go index 3426eab4..3cedb4fc 100644 --- a/internal/modules/master/phase-activities/services/phase-activity.service.go +++ b/internal/modules/master/phase-activities/services/phase-activity.service.go @@ -41,7 +41,8 @@ func NewPhaseActivityService(repo repository.PhaseActivityRepository, phaseRepo } func (s phaseActivityService) withRelations(db *gorm.DB) *gorm.DB { - return db + return db.Joins("JOIN phases ON phases.id = phase_activities.phase_id"). + Where("phases.deleted_at IS NULL") } func (s phaseActivityService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.PhaseActivity, int64, error) {