From dded9e807b42d61ecae4cf3b1a55d3fa4507c985 Mon Sep 17 00:00:00 2001 From: MacBook Air M1 Date: Tue, 6 Jan 2026 23:59:16 +0700 Subject: [PATCH 1/4] add api check uncheck assignment --- .../controllers/daily-checklist.controller.go | 18 +++++++++ internal/modules/daily-checklists/route.go | 16 ++++++++ .../services/daily-checklist.service.go | 37 +++++++++++++++++++ .../validations/daily-checklist.validation.go | 7 ++++ 4 files changed, 78 insertions(+) diff --git a/internal/modules/daily-checklists/controllers/daily-checklist.controller.go b/internal/modules/daily-checklists/controllers/daily-checklist.controller.go index b5a9b7b5..351c408b 100644 --- a/internal/modules/daily-checklists/controllers/daily-checklist.controller.go +++ b/internal/modules/daily-checklists/controllers/daily-checklist.controller.go @@ -241,3 +241,21 @@ func (u *DailyChecklistController) GetAllTasks(c *fiber.Ctx) error { Data: result, }) } + +func (u *DailyChecklistController) UpdateAssignment(c *fiber.Ctx) error { + req := new(validation.UpdateAssignment) + if err := c.BodyParser(req); err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") + } + + if err := u.DailyChecklistService.UpdateAssignment(c, req); err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.Success{ + Code: fiber.StatusOK, + Status: "success", + Message: "Assignment updated successfully", + }) +} diff --git a/internal/modules/daily-checklists/route.go b/internal/modules/daily-checklists/route.go index c8542671..bffb5013 100644 --- a/internal/modules/daily-checklists/route.go +++ b/internal/modules/daily-checklists/route.go @@ -19,16 +19,32 @@ func DailyChecklistRoutes(v1 fiber.Router, u user.UserService, s dailyChecklist. route.Post("/", ctrl.CreateOne) // create task + /* + ketika add phase + */ route.Post("/phase/:idDailyChecklist", ctrl.CreateDailyChecklistPhase) // create assigment + /* + ketika add ABK + */ route.Post("/assignment/:idDailyChecklist", ctrl.CreateAssignment) + // remove assignment + /* + ketika remove ABK + */ route.Delete("/:idDailyChecklist/assignments/:idEmployee", ctrl.RemoveAssignment) //get all tasks route.Get("/tasks", ctrl.GetAllTasks) + // update assignment + /* + ketika check dan uncheck tugas oleh ABK + */ + route.Post("/assignment", ctrl.UpdateAssignment) + 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 index bf5320e6..63c3cc9c 100644 --- a/internal/modules/daily-checklists/services/daily-checklist.service.go +++ b/internal/modules/daily-checklists/services/daily-checklist.service.go @@ -29,6 +29,7 @@ type DailyChecklistService interface { 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) + UpdateAssignment(ctx *fiber.Ctx, req *validation.UpdateAssignment) error } type dailyChecklistService struct { @@ -296,6 +297,42 @@ func (s dailyChecklistService) GetTasks(c *fiber.Ctx, checklistID uint) ([]entit return tasks, nil } +func (s dailyChecklistService) UpdateAssignment(c *fiber.Ctx, req *validation.UpdateAssignment) error { + if err := s.Validate.Struct(req); err != nil { + return err + } + + task := new(entity.DailyChecklistActivityTask) + if err := s.Repository.DB().WithContext(c.Context()).First(task, req.TaskID).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, "Task not found") + } + return err + } + + if req.EmployeeID == 0 { + return fiber.NewError(fiber.StatusBadRequest, "Invalid employee id") + } + + updates := map[string]any{"updated_at": time.Now()} + if req.Checked != nil { + updates["checked"] = *req.Checked + } + if req.Note != nil { + updates["note"] = *req.Note + } + + return s.Repository.DB().WithContext(c.Context()).Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "task_id"}, {Name: "employee_id"}}, + DoUpdates: clause.Assignments(updates), + }).Create(&entity.DailyChecklistActivityTaskAssignment{ + TaskId: req.TaskID, + EmployeeId: req.EmployeeID, + Checked: req.Checked != nil && *req.Checked, + Note: req.Note, + }).Error +} + func parsePhaseIDs(raw string) ([]uint, error) { parts := strings.Split(raw, ",") result := make([]uint, 0, len(parts)) diff --git a/internal/modules/daily-checklists/validations/daily-checklist.validation.go b/internal/modules/daily-checklists/validations/daily-checklist.validation.go index ba81fd0d..61e8a455 100644 --- a/internal/modules/daily-checklists/validations/daily-checklist.validation.go +++ b/internal/modules/daily-checklists/validations/daily-checklist.validation.go @@ -24,3 +24,10 @@ type AssignPhases struct { type AssignTask struct { EmployeeIDs string `json:"employee_ids" validate:"required"` } + +type UpdateAssignment struct { + TaskID uint `json:"task_id" validate:"required"` + EmployeeID uint `json:"employee_id" validate:"required"` + Checked *bool `json:"checked,omitempty"` + Note *string `json:"note,omitempty"` +} From 42aa6829c5a6fa66552939ee9a62827b957f6b45 Mon Sep 17 00:00:00 2001 From: MacBook Air M1 Date: Wed, 7 Jan 2026 10:36:40 +0700 Subject: [PATCH 2/4] add api detail daily checklist --- .../controllers/daily-checklist.controller.go | 32 ++++- .../dto/daily-checklist.dto.go | 123 +++++++++++++++- internal/modules/daily-checklists/route.go | 9 +- .../services/daily-checklist.service.go | 136 +++++++++++++++++- 4 files changed, 288 insertions(+), 12 deletions(-) diff --git a/internal/modules/daily-checklists/controllers/daily-checklist.controller.go b/internal/modules/daily-checklists/controllers/daily-checklist.controller.go index 351c408b..a965208f 100644 --- a/internal/modules/daily-checklists/controllers/daily-checklist.controller.go +++ b/internal/modules/daily-checklists/controllers/daily-checklist.controller.go @@ -54,14 +54,14 @@ func (u *DailyChecklistController) GetAll(c *fiber.Ctx) error { } func (u *DailyChecklistController) GetOne(c *fiber.Ctx) error { - param := c.Params("id") + param := c.Params("idDailyChecklist") id, err := strconv.Atoi(param) if err != nil { return fiber.NewError(fiber.StatusBadRequest, "Invalid Id") } - result, err := u.DailyChecklistService.GetOne(c, uint(id)) + detail, err := u.DailyChecklistService.GetDetail(c, uint(id)) if err != nil { return err } @@ -71,7 +71,7 @@ func (u *DailyChecklistController) GetOne(c *fiber.Ctx) error { Code: fiber.StatusOK, Status: "success", Message: "Get dailyChecklist successfully", - Data: dto.ToDailyChecklistListDTO(*result), + Data: dto.ToDailyChecklistDetailDTO(detail.Checklist, detail.Phases, detail.Tasks, detail.AssignedEmployees, detail.TotalActivities, detail.Progress), }) } @@ -217,6 +217,32 @@ func (u *DailyChecklistController) RemoveAssignment(c *fiber.Ctx) error { }) } +func (u *DailyChecklistController) GetPhaseByIdChecklist(c *fiber.Ctx) error { + param := c.Params("idDailyChecklist") + id, err := strconv.Atoi(param) + if err != nil || id <= 0 { + return fiber.NewError(fiber.StatusBadRequest, "Invalid daily checklist id") + } + + phaseIDs, err := u.DailyChecklistService.GetChecklistPhaseIDs(c, uint(id)) + if err != nil { + return err + } + + responseData := make([]map[string]uint, len(phaseIDs)) + for i, phaseID := range phaseIDs { + responseData[i] = map[string]uint{"phase_id": phaseID} + } + + return c.Status(fiber.StatusOK). + JSON(response.Success{ + Code: fiber.StatusOK, + Status: "success", + Message: "Get phases successfully", + Data: responseData, + }) +} + func (u *DailyChecklistController) GetAllTasks(c *fiber.Ctx) error { checklistParam := c.Query("checklist_id", "") if checklistParam == "" { diff --git a/internal/modules/daily-checklists/dto/daily-checklist.dto.go b/internal/modules/daily-checklists/dto/daily-checklist.dto.go index 31953def..14c8ad7a 100644 --- a/internal/modules/daily-checklists/dto/daily-checklist.dto.go +++ b/internal/modules/daily-checklists/dto/daily-checklist.dto.go @@ -4,6 +4,10 @@ import ( "time" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + employeeDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/employees/dto" + kandangDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto" + phaseActivityDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phase-activities/dto" + phasesDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phasess/dto" userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" ) @@ -15,15 +19,48 @@ type DailyChecklistRelationDTO struct { } 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"` + 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"` } type DailyChecklistDetailDTO struct { DailyChecklistListDTO + Phases []DailyChecklistPhaseDTO `json:"phases"` + Tasks []DailyChecklistActivityTaskDTO `json:"tasks"` + AssignedEmployees []employeeDTO.EmployeesRelationDTO `json:"assigned_employees"` + TotalActivity int `json:"total_activity"` + Progress float64 `json:"progress"` +} + +type DailyChecklistPhaseDTO struct { + Id uint `json:"id"` + PhaseId uint `json:"phase_id"` + Phase phasesDTO.PhasesListDTO `json:"phase"` +} + +type DailyChecklistActivityTaskDTO struct { + Id uint `json:"id"` + ChecklistId uint `json:"checklist_id"` + PhaseId uint `json:"phase_id"` + PhaseActivityId uint `json:"phase_activity_id"` + TimeType *string `json:"time_type"` + Notes *string `json:"notes"` + Phase phasesDTO.PhasesListDTO `json:"phase"` + PhaseActivity phaseActivityDTO.PhaseActivityListDTO `json:"phase_activity"` + Assignments []DailyChecklistAssignmentDTO `json:"assignments"` +} + +type DailyChecklistAssignmentDTO struct { + Employee employeeDTO.EmployeesRelationDTO `json:"employee"` + Checked bool `json:"checked"` + Note *string `json:"note"` } // === Mapper Functions === @@ -52,9 +89,24 @@ func ToDailyChecklistListDTO(e entity.DailyChecklist) DailyChecklistListDTO { name = *e.Name } + var status string + if e.Status != nil { + status = *e.Status + } + + var kandang *kandangDTO.KandangRelationDTO + if e.Kandang.Id != 0 { + mapped := kandangDTO.ToKandangRelationDTO(e.Kandang) + kandang = &mapped + } + return DailyChecklistListDTO{ Id: e.Id, Name: name, + Status: status, + Category: e.Category, + Date: e.Date, + Kandang: kandang, CreatedAt: e.CreatedAt, UpdatedAt: e.UpdatedAt, CreatedUser: createdUser, @@ -69,8 +121,65 @@ func ToDailyChecklistListDTOs(e []entity.DailyChecklist) []DailyChecklistListDTO return result } -func ToDailyChecklistDetailDTO(e entity.DailyChecklist) DailyChecklistDetailDTO { +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 { + phaseDTOs = append(phaseDTOs, DailyChecklistPhaseDTO{ + Id: phase.Id, + PhaseId: phase.PhaseId, + Phase: phasesDTO.ToPhasesListDTO(phase.Phase), + }) + } + + taskDTOs := make([]DailyChecklistActivityTaskDTO, 0, len(tasks)) + for _, task := range tasks { + mappedAssignments := make([]DailyChecklistAssignmentDTO, 0, len(task.Assignments)) + for _, assignment := range task.Assignments { + if assignment.Employee.Id == 0 { + continue + } + mapped := DailyChecklistAssignmentDTO{ + Employee: employeeDTO.ToEmployeesRelationDTO(assignment.Employee), + Checked: assignment.Checked, + Note: assignment.Note, + } + mappedAssignments = append(mappedAssignments, mapped) + } + + phaseDTO := phasesDTO.PhasesListDTO{} + if task.Phase.Id != 0 { + phaseDTO = phasesDTO.ToPhasesListDTO(task.Phase) + } + + activityDTO := phaseActivityDTO.PhaseActivityListDTO{} + if task.PhaseActivity.Id != 0 { + activityDTO = phaseActivityDTO.ToPhaseActivityListDTO(task.PhaseActivity) + } + + taskDTOs = append(taskDTOs, DailyChecklistActivityTaskDTO{ + Id: task.Id, + ChecklistId: task.ChecklistId, + PhaseId: task.PhaseId, + PhaseActivityId: task.PhaseActivityId, + TimeType: task.TimeType, + Notes: task.Notes, + Phase: phaseDTO, + PhaseActivity: activityDTO, + Assignments: mappedAssignments, + }) + } + + assignedDTOs := make([]employeeDTO.EmployeesRelationDTO, 0, len(assignedEmployees)) + for _, emp := range assignedEmployees { + assignedDTOs = append(assignedDTOs, employeeDTO.ToEmployeesRelationDTO(emp)) + } + return DailyChecklistDetailDTO{ - DailyChecklistListDTO: ToDailyChecklistListDTO(e), + DailyChecklistListDTO: ToDailyChecklistListDTO(checklist), + Phases: phaseDTOs, + Tasks: taskDTOs, + AssignedEmployees: assignedDTOs, + TotalActivity: totalActivities, + Progress: progress, } } diff --git a/internal/modules/daily-checklists/route.go b/internal/modules/daily-checklists/route.go index bffb5013..d907143d 100644 --- a/internal/modules/daily-checklists/route.go +++ b/internal/modules/daily-checklists/route.go @@ -16,8 +16,16 @@ func DailyChecklistRoutes(v1 fiber.Router, u user.UserService, s dailyChecklist. route.Use(m.Auth(u)) route.Get("/", ctrl.GetAll) + + // create daily checklist route.Post("/", ctrl.CreateOne) + // get detail data daily checklist by id + route.Get("/relation/:idDailyChecklist", ctrl.GetOne) + + // get phases by daily checklist id + route.Get("/phase/:idDailyChecklist", ctrl.GetPhaseByIdChecklist) + // create task /* ketika add phase @@ -45,7 +53,6 @@ func DailyChecklistRoutes(v1 fiber.Router, u user.UserService, s dailyChecklist. */ route.Post("/assignment", ctrl.UpdateAssignment) - 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 index 63c3cc9c..97ea4523 100644 --- a/internal/modules/daily-checklists/services/daily-checklist.service.go +++ b/internal/modules/daily-checklists/services/daily-checklist.service.go @@ -2,6 +2,8 @@ package service import ( "errors" + "math" + "sort" "strconv" "strings" "time" @@ -30,6 +32,8 @@ type DailyChecklistService interface { RemoveAssignment(ctx *fiber.Ctx, id uint, employeeID uint) error GetTasks(ctx *fiber.Ctx, checklistID uint) ([]entity.DailyChecklistActivityTask, error) UpdateAssignment(ctx *fiber.Ctx, req *validation.UpdateAssignment) error + GetChecklistPhaseIDs(ctx *fiber.Ctx, checklistID uint) ([]uint, error) + GetDetail(ctx *fiber.Ctx, id uint) (*DailyChecklistDetail, error) } type dailyChecklistService struct { @@ -39,6 +43,15 @@ type dailyChecklistService struct { PhaseRepo phaseRepo.PhasesRepository } +type DailyChecklistDetail struct { + Checklist entity.DailyChecklist + Phases []entity.DailyChecklistPhase + Tasks []entity.DailyChecklistActivityTask + AssignedEmployees []entity.Employee + TotalActivities int + Progress float64 +} + func NewDailyChecklistService(repo repository.DailyChecklistRepository, phaseRepo phaseRepo.PhasesRepository, validate *validator.Validate) DailyChecklistService { return &dailyChecklistService{ Log: utils.Log, @@ -49,7 +62,7 @@ func NewDailyChecklistService(repo repository.DailyChecklistRepository, phaseRep } func (s dailyChecklistService) withRelations(db *gorm.DB) *gorm.DB { - return db + return db.Preload("Kandang") } func (s dailyChecklistService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.DailyChecklist, int64, error) { @@ -86,6 +99,72 @@ func (s dailyChecklistService) GetOne(c *fiber.Ctx, id uint) (*entity.DailyCheck return dailyChecklist, nil } +func (s dailyChecklistService) GetDetail(c *fiber.Ctx, id uint) (*DailyChecklistDetail, error) { + checklist, err := s.GetOne(c, id) + if err != nil { + return nil, err + } + + db := s.Repository.DB().WithContext(c.Context()) + + var phases []entity.DailyChecklistPhase + if err := db. + Where("checklist_id = ?", id). + Preload("Phase", func(tx *gorm.DB) *gorm.DB { + return tx.Preload("Activities") + }). + Order("created_at ASC"). + Find(&phases).Error; err != nil { + s.Log.Errorf("Failed to get phases for daily checklist %d: %+v", id, err) + return nil, err + } + + var tasks []entity.DailyChecklistActivityTask + if err := db. + Where("checklist_id = ?", id). + Preload("Phase"). + Preload("PhaseActivity"). + Preload("Assignments", func(tx *gorm.DB) *gorm.DB { + return tx.Preload("Employee") + }). + Order("created_at ASC"). + Find(&tasks).Error; err != nil { + s.Log.Errorf("Failed to get tasks for daily checklist %d: %+v", id, err) + return nil, err + } + + assignedEmployees := collectAssignedEmployees(tasks) + + totalActivities := 0 + for _, phase := range phases { + totalActivities += len(phase.Phase.Activities) + } + + var totalAssignments, completedAssignments int + for _, task := range tasks { + for _, assignment := range task.Assignments { + totalAssignments++ + if assignment.Checked { + completedAssignments++ + } + } + } + + var progress float64 + if totalAssignments > 0 { + progress = math.Round((float64(completedAssignments) / float64(totalAssignments)) * 100) + } + + return &DailyChecklistDetail{ + Checklist: *checklist, + Phases: phases, + Tasks: tasks, + AssignedEmployees: assignedEmployees, + TotalActivities: totalActivities, + Progress: progress, + }, nil +} + func (s *dailyChecklistService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.DailyChecklist, error) { if err := s.Validate.Struct(req); err != nil { return nil, err @@ -297,6 +376,35 @@ func (s dailyChecklistService) GetTasks(c *fiber.Ctx, checklistID uint) ([]entit return tasks, nil } +func (s dailyChecklistService) GetChecklistPhaseIDs(c *fiber.Ctx, checklistID uint) ([]uint, error) { + if checklistID == 0 { + return nil, fiber.NewError(fiber.StatusBadRequest, "checklist_id is required") + } + + if _, err := s.Repository.GetByID(c.Context(), checklistID, nil); err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusNotFound, "DailyChecklist not found") + } + return nil, err + } + + var phases []entity.DailyChecklistPhase + if err := s.Repository.DB().WithContext(c.Context()). + Where("checklist_id = ?", checklistID). + Order("created_at ASC"). + Find(&phases).Error; err != nil { + s.Log.Errorf("Failed to get daily checklist phases: %+v", err) + return nil, err + } + + phaseIDs := make([]uint, len(phases)) + for i, p := range phases { + phaseIDs[i] = p.PhaseId + } + + return phaseIDs, nil +} + func (s dailyChecklistService) UpdateAssignment(c *fiber.Ctx, req *validation.UpdateAssignment) error { if err := s.Validate.Struct(req); err != nil { return err @@ -392,6 +500,32 @@ func collectTaskIDs(tasks []entity.DailyChecklistActivityTask) []uint { } return result } + +func collectAssignedEmployees(tasks []entity.DailyChecklistActivityTask) []entity.Employee { + employeeMap := make(map[uint]entity.Employee) + for _, task := range tasks { + for _, assignment := range task.Assignments { + if assignment.Employee.Id == 0 { + continue + } + if _, exists := employeeMap[assignment.Employee.Id]; exists { + continue + } + employeeMap[assignment.Employee.Id] = assignment.Employee + } + } + + employees := make([]entity.Employee, 0, len(employeeMap)) + for _, emp := range employeeMap { + employees = append(employees, emp) + } + + sort.Slice(employees, func(i, j int) bool { + return employees[i].Id < employees[j].Id + }) + + return employees +} func (s dailyChecklistService) AssignTasks(c *fiber.Ctx, id uint, req *validation.AssignTask) error { if err := s.Validate.Struct(req); err != nil { return err From e545047165f53ffedce77369333ca495ba180a19 Mon Sep 17 00:00:00 2001 From: MacBook Air M1 Date: Wed, 7 Jan 2026 12:02:44 +0700 Subject: [PATCH 3/4] add api summary and update status --- .../controllers/daily-checklist.controller.go | 77 ++++++++++++- .../dto/daily-checklist.dto.go | 20 ++++ internal/modules/daily-checklists/route.go | 4 +- .../services/daily-checklist.service.go | 106 +++++++++++++++++- .../validations/daily-checklist.validation.go | 10 +- 5 files changed, 208 insertions(+), 9 deletions(-) diff --git a/internal/modules/daily-checklists/controllers/daily-checklist.controller.go b/internal/modules/daily-checklists/controllers/daily-checklist.controller.go index a965208f..7f2aabe9 100644 --- a/internal/modules/daily-checklists/controllers/daily-checklist.controller.go +++ b/internal/modules/daily-checklists/controllers/daily-checklist.controller.go @@ -53,6 +53,81 @@ func (u *DailyChecklistController) GetAll(c *fiber.Ctx) error { }) } +func (u *DailyChecklistController) GetSummary(c *fiber.Ctx) error { + query := &validation.SummaryQuery{ + DateFrom: c.Query("date_from"), + DateTo: c.Query("date_to"), + Category: c.Query("category"), + } + + if query.DateFrom == "" || query.DateTo == "" { + return fiber.NewError(fiber.StatusBadRequest, "date_from and date_to are required") + } + + if kandangParam := c.Query("kandang_id"); kandangParam != "" { + kandangID, err := strconv.ParseUint(kandangParam, 10, 64) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid kandang_id") + } + value := uint(kandangID) + query.KandangID = &value + } + + result, err := u.DailyChecklistService.GetSummary(c, query) + if err != nil { + return err + } + + type summaryResponse struct { + PerformanceOverview []dto.DailyChecklistPerformanceOverviewDTO `json:"performance_overview"` + TrackingABK []dto.DailyChecklistSummaryDTO `json:"tracking_abk"` + } + + performanceMap := make(map[uint]*dto.DailyChecklistPerformanceOverviewDTO) + tracking := make([]dto.DailyChecklistSummaryDTO, len(result)) + + for i, summary := range result { + tracking[i] = dto.DailyChecklistSummaryDTO{ + EmployeeID: summary.EmployeeID, + EmployeeName: summary.EmployeeName, + KandangID: summary.KandangID, + KandangName: summary.KandangName, + TotalActivity: summary.TotalActivity, + ActivityDone: summary.ActivityDone, + ActivityLeft: summary.ActivityLeft, + CompletionRate: summary.CompletionRate, + LastActivity: summary.LastActivity, + } + + if _, ok := performanceMap[summary.EmployeeID]; !ok { + performanceMap[summary.EmployeeID] = &dto.DailyChecklistPerformanceOverviewDTO{ + EmployeeID: summary.EmployeeID, + EmployeeName: summary.EmployeeName, + } + } + + performanceMap[summary.EmployeeID].TotalActivity += summary.TotalActivity + performanceMap[summary.EmployeeID].ActivityDone += summary.ActivityDone + performanceMap[summary.EmployeeID].ActivityLeft += summary.ActivityLeft + } + + performance := make([]dto.DailyChecklistPerformanceOverviewDTO, 0, len(performanceMap)) + for _, v := range performanceMap { + performance = append(performance, *v) + } + + return c.Status(fiber.StatusOK). + JSON(response.Success{ + Code: fiber.StatusOK, + Status: "success", + Message: "Get daily checklist summary successfully", + Data: summaryResponse{ + PerformanceOverview: performance, + TrackingABK: tracking, + }, + }) +} + func (u *DailyChecklistController) GetOne(c *fiber.Ctx) error { param := c.Params("idDailyChecklist") @@ -98,7 +173,7 @@ func (u *DailyChecklistController) CreateOne(c *fiber.Ctx) error { func (u *DailyChecklistController) UpdateOne(c *fiber.Ctx) error { req := new(validation.Update) - 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 14c8ad7a..eea51184 100644 --- a/internal/modules/daily-checklists/dto/daily-checklist.dto.go +++ b/internal/modules/daily-checklists/dto/daily-checklist.dto.go @@ -39,6 +39,26 @@ type DailyChecklistDetailDTO struct { Progress float64 `json:"progress"` } +type DailyChecklistSummaryDTO struct { + EmployeeID uint `json:"employee_id"` + EmployeeName string `json:"employee_name"` + KandangID uint `json:"kandang_id"` + KandangName string `json:"kandang_name"` + TotalActivity int `json:"total_activity"` + ActivityDone int `json:"activity_done"` + ActivityLeft int `json:"activity_left"` + CompletionRate int `json:"completion_rate"` + LastActivity *time.Time `json:"last_activity,omitempty"` +} + +type DailyChecklistPerformanceOverviewDTO struct { + EmployeeID uint `json:"employee_id"` + EmployeeName string `json:"employee_name"` + TotalActivity int `json:"total_activity"` + ActivityDone int `json:"activity_done"` + ActivityLeft int `json:"activity_left"` +} + type DailyChecklistPhaseDTO struct { Id uint `json:"id"` PhaseId uint `json:"phase_id"` diff --git a/internal/modules/daily-checklists/route.go b/internal/modules/daily-checklists/route.go index d907143d..72396092 100644 --- a/internal/modules/daily-checklists/route.go +++ b/internal/modules/daily-checklists/route.go @@ -17,6 +17,8 @@ func DailyChecklistRoutes(v1 fiber.Router, u user.UserService, s dailyChecklist. route.Get("/", ctrl.GetAll) + route.Get("/summary", ctrl.GetSummary) + // create daily checklist route.Post("/", ctrl.CreateOne) @@ -53,6 +55,6 @@ func DailyChecklistRoutes(v1 fiber.Router, u user.UserService, s dailyChecklist. */ route.Post("/assignment", ctrl.UpdateAssignment) - route.Patch("/:id", ctrl.UpdateOne) + route.Patch("/:idDailyChecklist", 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 index 97ea4523..067b39b3 100644 --- a/internal/modules/daily-checklists/services/daily-checklist.service.go +++ b/internal/modules/daily-checklists/services/daily-checklist.service.go @@ -34,6 +34,7 @@ type DailyChecklistService interface { UpdateAssignment(ctx *fiber.Ctx, req *validation.UpdateAssignment) error GetChecklistPhaseIDs(ctx *fiber.Ctx, checklistID uint) ([]uint, error) GetDetail(ctx *fiber.Ctx, id uint) (*DailyChecklistDetail, error) + GetSummary(ctx *fiber.Ctx, params *validation.SummaryQuery) ([]DailyChecklistSummary, error) } type dailyChecklistService struct { @@ -52,6 +53,18 @@ type DailyChecklistDetail struct { Progress float64 } +type DailyChecklistSummary struct { + EmployeeID uint + EmployeeName string + KandangID uint + KandangName string + TotalActivity int + ActivityDone int + ActivityLeft int + CompletionRate int + LastActivity *time.Time +} + func NewDailyChecklistService(repo repository.DailyChecklistRepository, phaseRepo phaseRepo.PhasesRepository, validate *validator.Validate) DailyChecklistService { return &dailyChecklistService{ Log: utils.Log, @@ -202,14 +215,12 @@ func (s dailyChecklistService) UpdateOne(c *fiber.Ctx, req *validation.Update, i return nil, err } - updateBody := make(map[string]any) - - if req.Name != nil { - updateBody["name"] = *req.Name + updateBody := map[string]any{ + "status": req.Status, } - if len(updateBody) == 0 { - return s.GetOne(c, id) + if req.RejectReason != nil { + updateBody["reject_reason"] = *req.RejectReason } if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil { @@ -579,3 +590,86 @@ func (s dailyChecklistService) AssignTasks(c *fiber.Ctx, id uint, req *validatio return nil } + +func (s dailyChecklistService) GetSummary(c *fiber.Ctx, params *validation.SummaryQuery) ([]DailyChecklistSummary, error) { + if err := s.Validate.Struct(params); err != nil { + return nil, err + } + + dateFrom, err := time.Parse("2006-01-02", params.DateFrom) + if err != nil { + return nil, fiber.NewError(fiber.StatusBadRequest, "invalid date_from format, use YYYY-MM-DD") + } + + dateTo, err := time.Parse("2006-01-02", params.DateTo) + if err != nil { + return nil, fiber.NewError(fiber.StatusBadRequest, "invalid date_to format, use YYYY-MM-DD") + } + + type summaryRow struct { + EmployeeID uint + EmployeeName string + KandangID uint + KandangName string + TotalActivity int64 + ActivityDone int64 + ActivityLeft int64 + LastActivity *time.Time + } + + rows := make([]summaryRow, 0) + db := s.Repository.DB().WithContext(c.Context()). + Table("daily_checklist_activity_task_assignments AS a"). + Select(` + a.employee_id, + e.name AS employee_name, + d.kandang_id, + k.name AS kandang_name, + COUNT(*) AS total_activity, + SUM(CASE WHEN a.checked THEN 1 ELSE 0 END) AS activity_done, + SUM(CASE WHEN NOT a.checked THEN 1 ELSE 0 END) AS activity_left, + MAX(a.updated_at) AS last_activity`). + Joins("JOIN daily_checklist_activity_tasks t ON t.id = a.task_id"). + Joins("JOIN daily_checklists d ON d.id = t.checklist_id"). + Joins("JOIN kandangs k ON k.id = d.kandang_id"). + Joins("JOIN employees e ON e.id = a.employee_id"). + Where("d.date BETWEEN ? AND ?", dateFrom, dateTo) + + if params.Category != "" { + db = db.Where("d.category = ?", params.Category) + } + + if params.KandangID != nil { + db = db.Where("d.kandang_id = ?", *params.KandangID) + } + + if err := db. + Group("a.employee_id, e.name, d.kandang_id, k.name"). + Order("e.name ASC"). + Find(&rows).Error; err != nil { + s.Log.Errorf("Failed to get daily checklist summary: %+v", err) + return nil, err + } + + summaries := make([]DailyChecklistSummary, len(rows)) + for i, row := range rows { + completionRate := 0 + if row.TotalActivity > 0 { + completionRate = int(math.Round(float64(row.ActivityDone) / float64(row.TotalActivity) * 100)) + } + + summaries[i] = DailyChecklistSummary{ + EmployeeID: row.EmployeeID, + EmployeeName: row.EmployeeName, + KandangID: row.KandangID, + KandangName: row.KandangName, + TotalActivity: int(row.TotalActivity), + ActivityDone: int(row.ActivityDone), + ActivityLeft: int(row.ActivityLeft), + CompletionRate: completionRate, + LastActivity: row.LastActivity, + } + } + + return summaries, nil +} diff --git a/internal/modules/daily-checklists/validations/daily-checklist.validation.go b/internal/modules/daily-checklists/validations/daily-checklist.validation.go index 61e8a455..969c04e2 100644 --- a/internal/modules/daily-checklists/validations/daily-checklist.validation.go +++ b/internal/modules/daily-checklists/validations/daily-checklist.validation.go @@ -8,7 +8,8 @@ type Create struct { } type Update struct { - Name *string `json:"name,omitempty" validate:"omitempty"` + Status string `json:"status" validate:"required"` + RejectReason *string `json:"reject_reason" validate:"required"` } type Query struct { @@ -31,3 +32,10 @@ type UpdateAssignment struct { Checked *bool `json:"checked,omitempty"` Note *string `json:"note,omitempty"` } + +type SummaryQuery struct { + DateFrom string `query:"date_from" validate:"required"` + DateTo string `query:"date_to" validate:"required"` + Category string `query:"category" validate:"omitempty"` + KandangID *uint `query:"kandang_id" validate:"omitempty"` +} From c3f8ae5887c44470a662e8aa5ba9568abc486c8c Mon Sep 17 00:00:00 2001 From: MacBook Air M1 Date: Wed, 7 Jan 2026 17:39:17 +0700 Subject: [PATCH 4/4] 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) {