Files
lti-api/internal/modules/daily-checklists/services/daily-checklist.service.go
T
2026-01-07 12:02:44 +07:00

676 lines
19 KiB
Go

package service
import (
"errors"
"math"
"sort"
"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)
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 {
Log *logrus.Logger
Validate *validator.Validate
Repository repository.DailyChecklistRepository
PhaseRepo phaseRepo.PhasesRepository
}
type DailyChecklistDetail struct {
Checklist entity.DailyChecklist
Phases []entity.DailyChecklistPhase
Tasks []entity.DailyChecklistActivityTask
AssignedEmployees []entity.Employee
TotalActivities int
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,
Validate: validate,
Repository: repo,
PhaseRepo: phaseRepo,
}
}
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) {
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) 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
}
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 := map[string]any{
"status": req.Status,
}
if req.RejectReason != nil {
updateBody["reject_reason"] = *req.RejectReason
}
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 (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
}
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))
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 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
}
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
}
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
}